게시글 생성을 구현할 때 나중에 적용하기로 하고 넘어갔던 image-carousel
을 직접 구현했습니다.
처음에는 react-slick
을 적용하려고 했지만, 사용법에 대해 공부하기보다는 원리를 먼저 이해하고 직접 구현해 본 뒤 라이브러리를 사용하는 게 더 좋다고 생각해서 구글링을 통해서 원리를 이해하고 직접 구현해 봤습니다.
본인이 이해한 대로 정리
1. 제일 처음에는 제일 마지막 이미지를 추가한다.
2. 제일 끝에는 제일 처음 이미지를 추가한다.
3. 이미지들을 줄바꿈 하지 않고 가로로 배치한다. ( overflow: hidden
)
4. 우측 버튼을 누르면 transform: translateX(100%)
로 이동한다. ( transition
적용 ), ( 좌측은 반대로 )
추가적으로 알아야 할 것
1. 우측이든 좌측이든 끝에서 버튼을 누를 경우 최초에 추가했던 이미지를 보여준다. ( 위쪽 정리의 1, 2를 한 이유 )
2. 이미지 이동이 끝나고 난 뒤 transition
을 잠시 끄고 처음 이미지로 이동한 뒤 transition
을 킨다.
이해를 위한 설명
이미지가 만약 1, 2, 3이라는 이름으로 있다면 [3-1, 1, 2, 3, 1-1]
순으로 배치를 합니다.
( 설명을 위해 3-1
, 1-1
로 표현했습니다. )
그러고 나서 3
에서 우측으로 이동 시 1-1
로 이동한다.
1-1
의 transition
이 끝나고 나면 transition
을 끄고 1-1
에서 1
로 이동하고 다시 transition
을 켜줍니다.
그렇게 되면 사용자에게는 변화가 없고 내부적으로 처음인 1
로 이동했으므로 무한하게 이미지를 보여줄 수 있게 됩니다.
반대로 이동하는 경우에도 같은 형식으로 만들면 됩니다.
이후에 추가적으로 이미지 이동을 위한 화살표나 현재 위치를 알려줄 수 있는 변수를 추가해줬습니다.
// 2021/12/23 - image-carousel ( 게시글 읽기 모달 and 게시글 생성 모달에 사용 ) - by 1-blue
import React, { useCallback, useEffect, useRef, useState } from "react";
import Proptypes from "prop-types";
// styled-components
import { Wrapper } from "./style";
const ImageCarousel = ({ children, speed, length, height }) => {
const wrapperRef = useRef(null);
const dotRef = useRef(null);
const [imageNodes, setImageNodes] = useState(null);
const [dotNodes, setDotNodes] = useState(null);
const [currentIndex, setCurrentIndex] = useState(1);
const [click, setClick] = useState(true);
// 2021/12/23 - 이미지 노드들 배열로 모아서 state에 넣는 함수 - by 1-blue
useEffect(() => {
setImageNodes([...wrapperRef.current.childNodes]);
}, [wrapperRef.current]);
// 2021/12/23 - 첫 이미지 지정 - by 1-blue
useEffect(() => {
imageNodes?.forEach(imageNode => (imageNode.style.transform = `translateX(-${currentIndex * 100}%)`));
setTimeout(() => {
imageNodes?.forEach(imageNode => (imageNode.style.transition = `all ${speed}ms`));
}, 100);
}, [imageNodes]);
// 2021/12/23 - 다음 이미지로 넘기는 함수 - by 1-blue
const onClickNextButton = useCallback(() => {
if (!click) return;
// dot 모두 초기화 ( 이전에 이동이 앞인지 뒤인지 알 수 없으니 모두 초기화 )
dotNodes.forEach(dotNode => (dotNode.style.color = "white"));
// 이미지 변경
imageNodes.forEach(imageNode => (imageNode.style.transform = `translateX(-${(currentIndex + 1) * 100}%)`));
setCurrentIndex(prev => (prev + 1 === imageNodes.length - 1 ? 1 : prev + 1));
// 마지막 이미지에서 다음버튼을 누를 경우 실행
if (currentIndex + 1 === imageNodes.length - 1) {
setClick(false);
setTimeout(() => {
imageNodes.forEach(imageNode => (imageNode.style.transition = `all 0s`));
}, 900);
setTimeout(() => {
imageNodes.forEach(imageNode => (imageNode.style.transform = `translateX(-${1 * 100}%)`));
}, 1000);
setTimeout(() => {
imageNodes.forEach(imageNode => (imageNode.style.transition = `all ${speed}ms`));
setClick(true);
}, 1010);
// 현재 이미지와 dot 동기화
dotNodes[currentIndex - length].style.color = "black";
} else {
// 현재 이미지와 dot 동기화
dotNodes[currentIndex].style.color = "black";
}
}, [imageNodes, currentIndex, click, dotNodes, length]);
// 2021/12/23 - 이전 이미지로 넘기는 함수 - by 1-blue
const onClickPrevButton = useCallback(() => {
if (!click) return;
// dot 모두 초기화 ( 이전에 이동이 앞인지 뒤인지 알 수 없으니 모두 초기화 )
dotNodes.forEach(dotNode => (dotNode.style.color = "white"));
imageNodes.forEach(imageNode => (imageNode.style.transform = `translateX(-${(currentIndex - 1) * 100}%)`));
setCurrentIndex(prev => (prev - 1 === 0 ? imageNodes.length - 2 : prev - 1));
// 첫 이미지에서 이전버튼을 누를 경우 실행
if (currentIndex - 1 === 0) {
setClick(false);
setTimeout(() => {
imageNodes.forEach(imageNode => (imageNode.style.transition = `all 0s`));
}, 250);
setTimeout(() => {
imageNodes.forEach(imageNode => (imageNode.style.transform = `translateX(-${(imageNodes.length - 2) * 100}%)`));
}, 500);
setTimeout(() => {
imageNodes.forEach(imageNode => (imageNode.style.transition = `all ${speed}ms`));
setClick(true);
}, 510);
// 현재 이미지와 dot 동기화
dotNodes[length - 1].style.color = "black";
} else {
// 현재 이미지와 dot 동기화
dotNodes[currentIndex - 2].style.color = "black";
}
}, [imageNodes, currentIndex, click, dotNodes, length]);
// 2021/12/23 - dot 노드들 배열로 모아서 state에 넣는 함수들 - by 1-blue
useEffect(() => {
setDotNodes([...dotRef.current.childNodes]);
}, [dotRef.current]);
// 2021/12/23 - 첫 이미지와 dot 동기화 - by 1-blue
useEffect(() => {
if (!dotNodes) return;
dotNodes[0].style.color = "black";
}, [dotNodes]);
return (
<Wrapper height={height}>
{/* 이미지들 */}
<ul ref={wrapperRef} className="image-container">
{children}
</ul>
{/* 이미지 이동 버튼 */}
<button type="button" onClick={onClickNextButton} className="next-button">
{">"}
</button>
<button type="button" onClick={onClickPrevButton} className="prev-button">
{"<"}
</button>
{/* 이미지 현재 위치를 표시하는 노드들 */}
<ul className="dots" ref={dotRef}>
{Array(length)
.fill()
.map((v, i) => (
<li key={i}>•</li>
))}
</ul>
<span className="image-number">{`${currentIndex} / ${length}`}</span>
</Wrapper>
);
};
ImageCarousel.propTypes = {
children: Proptypes.node.isRequired,
speed: Proptypes.number,
height: Proptypes.number,
};
ImageCarousel.defaultProps = {
speed: 1000,
height: 100,
};
export default ImageCarousel;
구현할 때 기능을 구현하는데 어려움보다는 이미지를 배치하는 위치가 마음대로 되지 않아서 시간을 많이 허비했습니다.
또한 transition
을 끄고 이미지를 이동시키는데도 자꾸 transition
이 적용되는 문제가 발생해서 정확한 원인을 파악하지 못하고 해결을 위해 setTimeout
을 이용해서 순차적으로 실행이 되도록 만들었습니다.
또한 currentIndex
라는 state
를 이용해서 현재 이미지가 무엇인지 판단하도록 만들었는데 currentIndex
값에 +1
인지 +2
인지 어떤 계산을 적용해 줘야 정상적으로 작동할지 너무 헷갈려서 이해보다는 하나하나 적용해 보면서 문제를 고쳐나갔습니다.