개인적으로 진행하고 있는 프로젝트중에 위 사진처럼 fade-in
, fade-out
의 효과가 있는 컴포넌트를 만들었습니다.
처음 기획을 할 때, 해당 컴포넌트는 라이브러리를 쓰는게 좋을 것 같았지만 리액트와 친해지는 것이 목표이기 때문에 직접 만들어보고자 시작하게 되었습니다.
많이 부족한 부분이 있는 컴포넌트지만 이 컴포넌트를 만들면서 어떤 고민을 했는지 어떻게 만들게 되었는지를 기록하고자 작성하게 되었습니다. (완전 우당탕탕 기록 입니다)
진행하고 있는 프로젝트는 이러한 환경에서 진행되고 있습니다.
1. React, TypeScript를 이용합니다.
2. emotion 라이브러리로 CSS-In-JS 형식을 이용 합니다.
가장 먼저 숫자를 어떻게 하나씩 보여줄까에 대해 중점으로 생각하였습니다.
(1) 들어온 숫자의 값을 number Array 로 바꾸어준다.
ex)456 -> [4,5,6]
(2)useState
를 이용하여showNumber
에for
문을 이용하여 해당 배열의 idx로 숫자를 셋팅해준다.
ex)setShowNumber(numbers[i]);
(3)for문
에서 순회하는 부분을setTimeout
혹은setInterval
을 이용하여일정 시간마다setShowNumber
가 되도록한다.
여기서 setTimeout
과 setInterval
두가지 중 어떤 함수를 이용해야 더 적절할지에 대해 생각을 했습니다.
setTimeout
: 일정 시간이 지난 후에 함수를 실행
일정 시간이 지난 후 다른 함수를 호출하는 데 사용되는 시간 이벤트 함수이지만 함수를 한 번만 실행합니다.
위의 그림은 중첩된 setTimeout
을 이용한 그림이지만 setInterval
과 차이가 확연히 보여지는 좋은 그림이라 가져왔습니다.
그림처럼 setTimeout
은 지연 간격을 보장합니다. 이 함수를 실행하는데에 몇초가 걸리든 꼭 지연 시간 이후에 해당 함수가 실행됩니다.
주로 사용자 입력 후 일정 시간 동안 입력 대기 후 동작을 실행하거나, 애니메이션 시작을 지연시키고 나중에 시작해야 할 때 해당 함수를 사용합니다.
setInterval
: 일정 시간 간격을 두고 함수를 실행특정 시간 후에 함수를 호출하며 지정된 시간 간격에 따라 실행이 연속적으로 발생합니다.
사진처럼 100ms 라는 지연 간격을 가지고 있지만, 함수가 실행되는데에 소모한 시간 까지도 100ms 에 포함시키기 때문에 지연 간격은 실제 100ms 보다 짧아집니다.
만약, 함수를 실행하는 데 걸리는 시간이 명시한 지연 간격보다 길다면 엔진이 함수의 실행이 종료될 때 까지 기다려준 후 스케줄러를 확인하고, 그 다음 호출을 바로 시작합니다.
따라서 함수 호출에 걸리는 시간이 지연 간격보다 길다면 지연 간격이 따로 없이 계속 호출하게 되는 현상을 가지게 될 수 있습니다.
주로 실시간 데이터 업데이트, 주기적으로 화면 갱신을 위한 애니메이션 루프, 실시간 채팅 시스템 에서 해당 함수를 많이 사용합니다.
최신 웹 개발에서는
setTimeout
을 활용하여 재귀적인 호출을 통해 반복적인 작업을 수행하는 방식을 더 선호 합니다. 작업이 시작된 후 일정시간이 지난후에 다음 작업이 스케줄링 되기 때문에 사이드이펙트를 방지하는데에 이점을 가져올 수 있습니다.
setTimeout
을 채택하다.우선 해당 컴포넌트는 일정한 시간을 가지고 숫자를 보여주어야 한다. (지연 간격 보장 필요) 분명한 목적이 있기 때문에 더 깊게 고민하지 않고 setTimeout
을 사용하여 컴포넌트를 작성을 시작합니다.
React Hook useEffect에 불필요한 종속성인 'ref.current'가 있습니다. 이를 제외하거나 의존성 배열을 제거하세요. 'ref.current'와 같은 변경 가능한 값은 변경해도 컴포넌트가 다시 렌더링되지 않으므로 유효한 의존성이 아닙니다. (react-hooks/exhaustive-deps)
처음 코드를 작성할 때, 단순히 값을 저장하는 용도의 useRef
의 값도 useEffect
의 종속성에 넣어야하나 고민을 했다가 해당 페이지에서 좋은 답변을 얻었습니다. (비록 페이지에서의 질문의 의도와는 다르지만요)
fade-in
, fade-out
을 적용하자.어찌저찌 숫자를 하나씩만 보여지도록 작성을 하였습니다.
그럼 이 숫자가 바뀔때 마다 fade-in
, fade-out
효과를 주기만 하면 끝이겠네요. (진짜 여기서 너무 너무 너무.... 너무 꼬아서 생각했는지 생각보다 시간이 많이 들었습니다 ㅠㅠ)
const fadeStyle = css`
.fade-in {
opacity: 1;
animation: fadeIn ease-in-out 1s;
}
.fade-out {
opacity: 0;
animation: fadeOut ease-in-out 1s;
}
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes fadeOut {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
`;
이런식으로 css를 작성해서 숫자가 바뀔 때 마다 fade-in
, fade-out
효과를 적용해주면 되지 않을까 했었습니다.
즉, 내가 원하는 순서대로 fade-in
, fade-out
을 적용해야하는데, 이 부분에 있어서 Promise
를 사용하기로 합니다.
async/await
Promise
는 setTimeout
비동기 작업을 순차적으로 실행하고 그 작업이 완료 되어야 다음 작업으로 넘어갈 수 있습니다.
따라서 Promise
와 async / await
을 사용하여 비동기 흐름을 관리하기 위해 채택 하였습니다.
useEffect(() => {
...
const setTimeNumber = async () => {
for (let i = 0; i < numbers.length; i++) {
await new Promise<void>((resolve) => {
timeouts.current[i] = setTimeout(() => {
setShowNumber(numbers[i]);
setFadeInOut((prevFadeInOut) =>
prevFadeInOut === 'fade-in' ? 'fade-out' : 'fade-in'
);
resolve();
}, 1500);
});
}
};
setTimeNumber();
return () => {
timeoutsCopy.forEach((val) => {
clearTimeout(val);
});
};
}, []);
...
Promise
객체를 생성합니다. resolve
를 통해 Promise
가 완료 됨을 알릴 수 있습니다.await
키워드를 사용하여 Promise
가 완료될 때 까지 기다립니다.setTimeout
을 사용하여 숫자를 배치하고 fadeInOut
을 업데이트 합니다. 그 후 다음 숫자로 넘어갑니다.resolve()
가 호출되면 해당 Promise
는 완료 됩니다.상상력은 풍부했으나.... 😭 의도한 바가 아닌 결과물이 계속 나오게 됩니다.
setFadeInOut((prevFadeInOut) =>
prevFadeInOut === 'fade-in' ? 'fade-out' : 'fade-in'
);
사실은 이 부분의 코드가 잘못 되었다고 생각하는데, 어떻게 맞추어야 할지에 대해 고민을 많이 했습니다. ㅠㅠ
그러던 중 react-transition-group 을 발견하게 됩니다.
리액트의 컴포넌트에 transition
효과를 쉽게 줄 수 있는 공식 라이브러리 라니, 해당 컴포넌트 뿐 아니라 프로젝트 내에서도 유용하게 쓸 것 같아 해당 라이브러리를 바로 설치 했었습니다.
React-transition-group 에서 제공 해주는 컴포넌트 중 CSSTransition
을 사용해보고자 하였습니다.
이 컴포넌트는 CSS 애니메이션을 적용할 수 있도록 지원하며 CSS 클래스를 추가, 제거 또는 변경하여 애니메이션을 적용 할 수 있습니다.
export function InstantNumber({ value }: Props) {
...
return (
<div>
<CSSTransition
in={showNumber !== null}
timeout={500}
classNames='fade'
unmountOnExit
>
<Txt css={fade} typography={'h2'}>
{showNumber}
</Txt>
</CSSTransition>
</div>
);
}
const fade = css`
&.fade-enter {
opacity: 0;
}
&.fade-enter-active {
opacity: 1;
transition: opacity 0.5s ease-in;
}
&.fade-exit {
opacity: 1;
}
&.fade-exit-active {
opacity: 0;
transition: opacity 0.5s ease-out;
}
`;
중간에 코드를 살짝 변경하긴 했으나 CSS Transition 을 이용하여 코드 작성을 하였습니다.
하지만 당연히 제가 의도한 결과가 나오지 않았습니다. (신나서 공부를 제대로 안하고 적용한 자의 최후 입니다)
이 라이브러리는 React 컴포넌트의 라이프사이클과 함께 동작하여 특정 상태 변화에 따라 CSS 클래스를 토글함으로써 CSS 애니메이션을 적용 할 수 있습니다.
즉, 컴포넌트가 마운트 ~ 언마운트 ~ 이런 형식이 되어야 제가 의도한대로 나오게 될 것 같았습니다.
그러나, 여기서 컴포넌트를 마운트, 언마운트 시킬 정도인가? 라는 의문점이 들게 됩니다.
진행하고 있는 프로젝트에서 이 컴포넌트를 만들게 된 이유는 숫자를 하나씩 보여주고 그 후에 Input 창에 해당 숫자를 역순으로 입력 하는 것 입니다.
따라서 이 숫자의 길이는 최대로 14자로 생각하고 있으며, 12번에 걸쳐서 문제를 보여지게 할 예정인데, 그 숫자의 길이마다, 매 문제마다 마운트, 언마운트 되는 것은 렌더링 성능을 저하시킬 수 있을 것 같아 좋은 방법이 아니라고 생각했습니다.
다시 작성한 코드를 전부 지우고 다른 방법을 생각해봅니다.
개인적으로 저는 기본 for문
을 좋아하는 편은 아닙니다. 여기서부터 생각을 다시 시작하게 됩니다.
그러면, Props
로 받는 숫자를 배열로 만들면, 그 idx
값 을 보여주면 되지 않을까?
예를들어 { numberArray[showNumberIdx] }
처럼 만들면 for
문을 안써도 될 것 같단 생각이 들었습니다.
1) idx를 +1 씩 업데이트도 해야하고....
2) fadeStyle 적용을 위해 fade-in / fade-out 상태 변경 해주어야하고...
3) 근데 이 idx는 배열의 길이보다 길면 안되고...
이 부분을 각각 useEffect
를 사용하여 코드를 작성 하였습니다.
useEffect(() => {
const fadeTimeout = setTimeout(() => {
setFadeProp((prevFadeProp) => ({
fade: prevFadeProp.fade === "fade-in" ? "fade-out" : "fade-in"
}));
}, FADE_INTERVAL_MS);
if (showNumberIdx >= showNumberArrayLength) {
clearTimeout(fadeTimeout);
return;
}
return () => clearTimeout(fadeTimeout);
}, [fadeProp, showNumberIdx, showNumberArrayLength]);
useEffect(() => {
const numberTimeout = setTimeout(() => {
setShowNumberIdx((prev) => prev + 1);
}, FADE_INTERVAL_MS * 3);
if (showNumberIdx >= showNumberArrayLength) {
clearTimeout(numberTimeout);
return;
}
return () => clearTimeout(numberTimeout);
}, [showNumberIdx, showNumberArrayLength]);
동작 순서를 fade-in/out
-> showNumber
-> fade-in/out
이렇게 잡아놓고, 첫번째 useEffect
부분을 fade-in/out
업데이트 되도록 진행 하였습니다.
그리고 두번째는 showNumber
의 숫자를 바꾸도록 진행 했습니다.
여기서 FADE_INTERVAL_MS
는 1500으로 설정하였습니다.
fade-in/out
의 transition
을 1.5s 로 잡아놓은 부분도 있고, 자연스럽게 fade-in/out 후 숫자가 보여질때 까지는 FADE_INTERVAL_MS * 3
이 적절하다고 생각하여 지연 간격을 설정하였습니다.
어찌저찌 의도한 바 대로 잘 굴러가네요.
하지만, 아쉬운 부분이 너무 많이 남는 컴포넌트 입니다. 지식의 부족으로 여기서 더 발전을 못한다니, 너무 아쉬워요. (ㅠㅠ)
꼭 리액트와 절친이 되어 이 컴포넌트를 다시 리팩토링 하고 싶은 욕심이 가득합니다.
사실은 너무나도 부끄러운 제 코드를 공개해야하지만... 앞서 말씀 드렸듯이 컴포넌트를 하나 만들때마다 많은 생각을 하는데 한번쯤은 저의 생각 순서를 글로 정리해보고 싶었습니다.
이 내용보다 더~ 많은 고민을 하는데(수많은 에러도 만나고..) 줄일려고 나름대로 애를 써봤는데, 줄여진게 맞나 생각이 드네요.
리액트와 친해지고싶어서 애는 쓰는데 마음대로 되지 않아서 너무 슬프지만, 열심히 나아가려고 합니다. 나중에는 이 컴포넌트를 리팩토링 할만큼 실력을 갖추고 다시 한번 글을 쓰면 재밌을 것 같네요. 😄