Skip to content

React 컴포넌트에 애니메이션을 적용해보자 🏃🏻💨

n-ryu edited this page Dec 14, 2022 · 27 revisions

현재까지의 상황

다이어그램 뷰 구현 상황

  • 사용자가 추가한 Todo들의 선후관계를 플로우차트 형태로 보여주는 다이어그램 뷰를 구 현했다.
  • 다이어그램 뷰의 X축은 전체적인 우선순위 순으로 정렬되고, Y축은 Todo간 선후관계의 위계를 나타낸다. (선후관계가 있다면, 나중에 해야하는 Todo의 Y 위치가 더 크다)
  • 토글 버튼을 통해서 다이어그램에 이미 완료된 Todo도 함께 표시할지, 아직 하지 않은 Todo들만 표시할지 결정할 수 있다.
  • 앞으로 다이어그램 뷰 내에서도 Todo를 생성, 수정하고, 선후관계도 즉석에서 추가, 제거가 가능한 기능을 추가할 예정이다.

🤔하지만 뭔가 부족한걸...?

0 NoAnimation

  • 다이어그램 뷰에 표시되는 Todo의 목록이 바뀔 때 어떤 변화가 일어나는 것인지 알기가 어렵다.
  • 지금은 이미 완료된 Todo 표기를 바꿀 때만 이런 트랜지션이 있지만, 다이어그램 뷰 내에서 추가, 삭제, 편집이 가능하다면 어떤 변화가 일어난 것인지 사용자에게 보여주어야 사용자가 자신의 입력 제대로 반영된 것인지 판단하기 쉽다.
  • 다이어그램을 구성하는 Todo Block과 선후관계 vertex들이 다이어그램에 추가되고 삭제될 때 애니메이션 있으면 사용자가 이해하기도 쉽고 미려한 UX를 제공할 수 있을 것이다!

다이어그램에 애니메이션을 넣어보자

일단 CSS transition 옵션부터 넣어보자

1 TransitionOnly

  • TodoBlock 컴포넌트와 TodoVertex 컴포넌트의 스타일에 transition: transform 1s를 추가했다.
  • 기본적으로 리액트에서 Array.map을 통해 생성된 컴포넌트들은 key값을 통해서 동일 컴포넌트인지 판별이 되므로, 표시 데이터 변화 전과 후 모두에 존재하는 컴포넌트들은 그 위치가 자연스럽게 트랜지션 애니메이션이 적용되는 것을 확인할 수 있다.
  • 하지만 기존에 없었던 컴포넌트가 생기거나 있었던 컴포넌트가 없어지는 경우, 그냥 추적할 객체 자체가 없었거나 사라지므로 애니메이션 없이 갑자기 Todo Block이나 Vertex가 사라지는 것을 확인할 수 있다.

생성되고 삭제되는 컴포넌트를 어떻게 추적할 수 있을까?

  • 현재 데이터 저장 방식의 문제

    State Structure without Animation

    • 기존에는 TodoList API에서 받아온 데이터를 다이어그램을 그리기 위한 데이터로 변환하고, 그 데이터를 직접 <Diagram> 컴포넌트에 상태로 보관했다.
    • 그리고 상태로 저장된 데이터를 .map()으로 JSX 컴포넌트로 변환해서 여러 다이어그램 요소들을 그려주게 된다.
    • 하지만 이렇게 데이터를 바로 상태로 보관을 하면, 새로운 요소가 추가되거나 삭제되는 것을 <Diagram> 컴포넌트는 전혀 모르므로, 생성이나 제거 등의 변화를 추적할 수 없다!
  • 변화를 고려한 데이터 저장 방식

    State Structure considering Animation

    • 요소의 추가, 삭제를 추적하려면 데이터를 바로 상태로 보관하는 것이 아니라, 어떤 데이터가 새로 생겼고, 없어졌는지를 판별하고, 판별 결과와 정보들을 묶고 그 결과의 합집합을 통째로 상태로 저장해 두어야 한다.
    • 이렇게 저장을 한 뒤, 새로 생겨나는 요소들은 생성 트랜지션(애니메이션)을, 없어지는 요소들은 제거 트랜지션(애니메이션)을 적용시켜주면 생성, 제거 애니메이션을 구현할 수 있다.
    • 변경 이전과 이후 데이터의 합집합을 변화상태(생성, 삭제, 불변)와 함께 보관하면 요소들의 변화 추적이 가능하므로, 애니메이션을 넣을 수 있을 것이다!

실제로 요소 상태 추적 로직을 구현해보자

  • 일단 기존 상태와 새로운 상태를 받아서 요소들의 변화를 추적해보자

      const mountData = [...incomingData].filter(([key]) => !currentData.has(key));
      const idleData = [...incomingData].filter(([key]) => currentData.has(key));
      const unmountData = [...currentData].filter(([key]) => !incomingData.has(key));
    • HashMap 구조로 인자로 입력되는 신규 데이터와 기존 데이터를 비교해서 생성되는 요소들, 유지되는 요소들, 제거되는 요소들을 우선 분류해보았다.
  • 새로운 변화가 있어도 이전의 변화도 알고는 있어야 한다

    • 위의 데이터 관리 로직 그림에서는 새로운 변화 (기존 데이터와 신규 데이터의 차이)만을 저장하한다. 하지만 트랜지션이 1초가 걸린다고 한다면 그 사이에 변화가 두 번, 세 번, 심지어 만 번도 일어날 수 있다는 문제가 있다!
    • 따라서 이전에 일어났던 변화도 기억을 해 두어야한다. 이전의 변화 상태값을 그대로 두고 사용하고, 신규 변화만을 이전 변화 상태값에 업데이트 하는 방식으로 구현하면 이전의 변화가 지워지지않고 유지되고, 같은 요소에 대해 변화 상태가 달라지면 이 역시도 갱신되게 된다.
      const resultTransitionData = new Map([...currentTransitionData]);
      idleData.forEach(([key, value]) => {
        const target = resultTransitionData.get(key);
        if (target === undefined) return;
        resultTransitionData.set(key, { ...target, props: value });
      });
    • 위의 코드처럼, 기존의 데이터를 우선 변화 상태를 얕은 복사를 하고, (idle=불변인 요소들에 대해서는 기존 값을 그대로 복사해주도록 했다.)
  • 언젠가 변화 상태가 스스로 변해야 한다

    • 요소가 mount=생성 변화상태이거나, unmount=제거 변화상태라면 입력 데이터의 변화가 없어도 언젠가 트랜지션이 완료되면 스스로 상태를 idle=불변으로 업데이트 하거나, 스스로를 전체 상태리스트에서 제거해야한다.
    • mount 요소의 경우, 일단 mount 상태를 적용해서 초기 style 값을 주고, setTimeout으로 직후에 바로 idle 상태를 적용해서 트랜지션이 일어나게 했다. (transition-duration이 style에 정의도어 있으므로, 생성 직후에 초기값을 주입하고, 바로 변화 목표값을 주입하면 duration대로 변화가 일어난다.)
        mountData.forEach(([key, value]) => {
          setTimeout(() => {
            setTransitionData((prev) => {
              const newState = new Map([...prev]);
              newState.set(key, { aniState: 'idle', props: value });
              return newState;
            });
          }, 0);
          resultTransitionData.set(key, { aniState: 'mount', props: value });
        });
    • unmount=제거 요소의 경우, unmount 상태를 적용해서 제거되는 목표 style을 적용시켜서 idle style에서 unmount style로 트랜지션이 발생하도록 했다. unmount 요소의 경우 트랜지션이 끝난 이후에는 전체 데이터 리스트에서 제거되어야 하므로, 트랜지션 시간 이후에 스스로 전체 목록에서 제거되도록 setTimeout을 설정해 주었다.
        unmountData.forEach(([key, value]) => {
          setTimeout(() => {
            setTransitionData((prev) => {
              const newState = new Map([...prev]);
              newState.delete(key);
              return newState;
            });
        }, duration);
        resultTransitionData.set(key, { aniState: 'unmount', props: value });
  • 변화 중인 요소가 또 변화한다면, 기존 timeout을 제거해야 한다

    • 만약 unmount 중인 데이터가 다시 입력되어서 mount가 된다면, 그냥 상태를 mount로 만드는 것으로는 불충분하다.

      앞선 unmount 업데이트 과정에서 일정 시간 후에 스스로를 전체 데이터 리스트에서 제거하도록 setTimeout을 설정해 두었으므로, mount가 된 이후에 갑자기 요소가 사라져버리게 된다!

    • 따라서 setTimeout을 설정할 때에, 해당 timeout을 데이터와 함께 저장해 두었다가, 다른 setTimeout을 신규 등록해야 하는 차례가 오면 clearTimeout으로 이전에 등록했던 timeout을 제거해주도록 코드를 추가했다.
        mountData.forEach(([key, value]) => {
          const target = resultTransitionData.get(key);
          if (target !== undefined) clearTimeout(target.timeout);
          const timeout = setTimeout(() => {
            setTransitionData((prev) => {
              const newState = new Map([...prev]);
              newState.set(key, { aniState: 'idle', props: value });
              return newState;
            });
          }, 0);
          resultTransitionData.set(key, { aniState: 'mount', props: value, timeout });
        });
        unmountData.forEach(([key, value]) => {
          const target = resultTransitionData.get(key);
          if (target !== undefined) clearTimeout(target.timeout);
          const timeout = setTimeout(() => {
            setTransitionData((prev) => {
              const newState = new Map([...prev]);
              newState.delete(key);
              return newState;
            });
        }, duration);
        resultTransitionData.set(key, { aniState: 'unmount', props: value, timeout });

구현한 결과를 한번 다이어그램 뷰에 적용해보자

2 Animation

  • 결과물을 다이어그램 뷰에 적용시켜 보았다. 생성, 제거 트랜지션은 단순하게 opacity값만을 조정했다.
  • 의도한 대로, Todo Block과 Vertex가 생성되고 추가할 때에 뿅 나타나는 것이 아니라 투명도가 변하면서 서서히 나타나고 서서히 없어지게 구현이 잘 되었다!

개선 또 개선!

  • Vertex가 생성 추가될 때, 연관된 Todo를 위치를 추적하도록 하자 3 MorePrecisePathAnimation 4 EditAnimation

  • 가끔씩 애니메이션이 스킵되는 문제가 있다? 5 EditAnimationWithBug 6 EditAnimationWithoutBug

💊 비타500

📌 프로젝트

🐾 개발 일지

🥑 그룹활동

🌴 멘토링
🥕 데일리 스크럼
🍒 데일리 개인 회고
🐥 주간 회고
👯 발표 자료

Clone this wiki locally