React) 커스텀훅의 동작원리와 애니메이션

ynnsuis·2023년 7월 26일
1

진행 했었던 프로젝트에서 아이디, 비밀번호 찾기 페이지를 구현할때 이런 기능이 있었다.

현재 페이지의 Variant 를 선택하고 보여주는 기능이였는데, variant라는 상태값과, 그것을 핸들링하고 보여주는 컴포넌트로 이루어져 있다. 구현 단계에서는 해당 페이지 내에 로직이 합쳐져 있어서

width : 50%;
border-bottom: 3px solid yellow;
trasform : translateX(variant === "아이디찾기" ? 0% : 100%);
transition : transform 0.3s;

애니메이션이 이렇게 간단하게 구현이 되었고 작동도 아주 잘되었다. 그런데 프로젝트를 끝내고 리팩토링을 진행하면서 문제가 생겼다.
이런 유형의 variant selector는 범용성이 좋아 보여서 추상화 시키기 위해 로직을 분리하여 커스텀훅으로 만들어 관리하고 싶었다. 그래서 일단 커스텀훅을 만들어 variant 상태값과 컴포넌트 부분으로 나누어 넣어봤다.

const useVariantSelector = () => {
	const [variant,setVariant] = useState('아이디');
    
    const VariantSelector = () => {
    	return (
          <VariantWrapper>
       		 <VariantBox>          
          		<VariantBoxItem
          		  active={variant === "아이디"}
          		  onClick={() => handleClick("아이디")}
          		>
            		아이디 찾기
          		</VariantBoxItem>    
                <VariantBoxItem
                  active={variant === "비밀번호"}
                  onClick={() => handleClick("비밀번호")}
                >
            		비밀번호 찾기
                </VariantBoxItem>
       		 </VariantBox>            
             <VariantNoticeBar
                variant={variant}
             />
      	</VariantWrapper>
      )
      
    return {variant,VariantSelector}
 }

이런식으로 작성해서 페이지에서 필요한 variant상태값과 VariantSelector 컴포넌트를 리턴해서 사용하려고 했다. 하지만...!

transform은 동작을하는데 transition 애니메이션이 동작을 하지 않았다.
분명 코드는 바뀌지 않았고 단순히 로직을 분리하여 커스텀훅에 넣은것이니 문제는 일반 컴포넌트에서의 리렌더링과 커스텀훅에서의 리렌더링이 다를거라는 가설을 세워봤다.

일반적인 컴포넌트에서는 상태가 변경되면 변경된 값으로 새로운 가상 DOM을 생성한후 이전 DOM과 비교하여 변경된 부분만 실제 DOM을 업데이트 하는식으로 작동한다. transition은 이때에 변화가 발생하는 css속성에 효과를 넣어준다.

반면에 커스텀훅 내에서 정의한 컴포넌트는 상태가 변경되면, 컴포넌트 내 함수들이 리렌더링될때마다 재정의 되듯이, 컴포넌트 자체가 아예 재정의 되어버린다. 그래서 컴포넌트가 mount될때 처럼 변화 이전의 css속성이 없기때문에 transition이 발동하지 않는것이라고 생각했다.

하지만 그렇다면 커스텀훅 내에서 정의한 컴포넌트를 메모이제이션 하면 이전에 정의된 컴포넌트를 재사용 하니까 정상적으로 동작하지 않을까 생각했다. 하지만 커스텀 훅 내에 정의된 VariantSelector 컴포넌트에 useCallback을 씌워줘도, 여전히 transition 속성은 동작을 하지 않았다. 메모이제이션을 해도 이전 상태에 대한 css속성을 기억하지 못하는것 같았다.

그래서 방식을 바꾸어서

animation: ${(props) =>
    props.variant === "아이디"
      ? "slide-left 0.3s forwards"
      : "slide-right 0.3s forwards"};

  @keyframes slide-left {
    from {
      transform: translateX(100%);
    }
    to {
      transform: translateX(0);
    }
  }

  @keyframes slide-right {
    from {
      transform: translateX(0);
    }
    to {
      transform: translateX(100%);
    }
  }

이렇게 키프레임으로 애니메이션 효과를 주는 것으로 바꿔서 해결했다.

이전처럼 동작은 잘되지만 한가지 문제가 다시 생겨버렸다. 처음 이 페이지에 진입하거나 새로고침을 하면

이런식으로 NoticeBar가 반대편에서 선택한 variant 값으로 슬라이드 시작한다. 분명 깔끔한 해결방법은 아니였다. 그래서 이문제를 해결하기위해 useRef를 사용하여 이전의 variant 값을 기억하여 새로운 variant값과 이전의 variant값이 다를경우에만 애니메이션이 작동하게 하여 해결하였다.

  animation: ${(props) =>
    props.variant === "아이디" && props.prevVariant === "비밀번호"
      ? "slide-left 0.3s forwards"
      : props.variant === "비밀번호" && props.prevVariant === "아이디"
      ? "slide-right 0.3s forwards"
      : "none"};

그리고 또 하나의 재미있는 트러블 이슈가 있었는데

onChange로 실시간 유효성 검사를하는 생년월일 input에 타이핑을 할때마다 NoticeBar가 미친듯이 움직이는 것이였다. 유저가 타이핑을 할 때마다 페이지가 리렌더링 되고, 이 페이지내에서 사용하는 커스텀훅은 새로운 인스턴스를 생성하고 그 안에 정의되어있는 VariantSelector 컴포넌트도 계속 생성되는 것이였다. 이 문제는 VariantSelector컴포넌트를 useCallback로 감싸 메모이제이션하여 해결하였다.

이제는 로직을 분리한 후에도 이전처럼 잘 작동하는 코드가 되었다. 사실 문제는 해결되었지만, 커스텀훅 내에서 컴포넌트를 정의해서 쓰는 사례 자체가 검색해도 잘 안나오고 Chat-GPT도 transition이 동작하지 않는 원리를 정확히 말해주지 않아서 내가 이해한 내용이 정확하지 않을수도 있다. 하지만 그냥 넘어갈 수도 있는 사소한 이슈를 깊게 고민해본 덕분에 리액트의 동작원리에 대해서 좀더 깊게 공부해볼 수 있게 되어서 정말 뜻깊은 시간이었던것 같다.

0개의 댓글