[task-dashboard] useTransition, useDeferredValue를 이용하여 사용성 높이기(feat. React18)

쏘소·2022년 9월 18일
0

프로젝트

목록 보기
1/18
post-custom-banner

프로젝트 소개

World-map-note는 보드에 일정 관리와 필터링을 할 수 있는 카드형 대시보드이다.

디자인은 깃헙을 참고하였다.

문제점

속도가 빠른 데스크톱 환경에서는 문제가 없었던 카드 필터링 기능이, 모바일 환경에서는 속도가 느렸다. 인풋창에 키워드가 입력되는 속도가 느리고 카드가 필터링 되어 UI로 보여지는데 자꾸 끊겼다.

React18 concurrent

이럴 때 보통 디바운싱이나 쓰로틀링 기법을 이용해서 일정 시간이 지날 경우 중간 중간 렌더링을 해줄 수 있도록 할 수 있다. 하지만 이는 근본적인 해결법은 아니다.

하지만 이번 react18 부터는 cocurrent 기능을 이용하여 긴급한 동작(버튼 클릭 등)과 그렇지 않은 동작(데이터를 불러와 띄워주는) 을 제어할 수 있게 되었다. Concurrent는 더욱 긴급한 업데이트가 이미 시작한 렌더링을 중단 할 수 있음을 의미한다.

다시 말해서, 긴급 업데이트는 타이핑, 버튼 클릭 등 즉각적으로 상태가 변화하기를 기대하는 업데이트이며, 전환 업데이트는 타이핑 시 연관 검색어 보여주기처럼 즉각적으로 업데이트가 되지 않아도 어색하지 않은 것이다.
이 때 긴급 업데이트는 전환 업데이트에 영향을 받으면 안되기 때문에 useTransition으로 전환 업데이트를 감싸주어 긴급 업데이트를 우선적으로 해줄 수 있도록 한다.

useTransition

import { useTransition } from 'react';

function TabContainer() {
  const [isPending, startTransition] = useTransition();
  // ...
}

useTransition은 위와 같이 사용해줄 수가 있다.

  • useTranstion은 아무런 인자도 받지 않는다.
  • useTransition은 isPendingstartTransition 두 가지 요소의 배열을 리턴한다.
  • isPending 은 pending transition 상태가 있는 경우 알려주는 역할을 한다.
  • startTransition 은 전환으로서의 상태 업데이트를 마크하는 역할을 한다.

useTransition 적용

적용 전 기존의 코드는 다음과 같다.

SearchInput.tsx

const SearchInput = () => {
  const [keyInput, setKeyInput] = useState('')
  const setFilterTasks = useSetRecoilState(filterTasksAtom)
  
const handleKeyInputChange = (e: ChangeEvent<HTMLInputElement>) => {
    setKeyInput(e.currentTarget.value)
  }

  useEffect(() => {
    if (filtering.filter) {
     setFilterTasks(filterContents(filtering.type, keyInput, boardsTasks))
    }
  }, [boardsTasks, filtering.filter, filtering.type, setFilterTasks, keyInput])
}

return (
        <input
          value={keyInput}
          onChange={handleKeyInputChange}
        />
            )

Board.tsx

const Board = () => {
  const filterTasks = useRecoilValue(filterTasksAtom)
    
  const processTasks = filtering.filter
    ? filterTasks[process]
    : boardsTasks[process]
  
  return (
    <ul>
       {processTasks?.map((cardTask: ITask, iCard) => {
           const cardKey = `card=${iCard}`
           return <BoardCard key={cardKey} cardTask={cardTask} index={iCard} />
       })}
    </ul>
  )
  }

SearchInput 컴포넌트에 입력한 키워드를 filterContents utils의 인자로 사용하여 카드 목록을 필터링 한다. 이후 Board 컴포넌트에서 이 필터링을 리스트로 띄워 보여주게 된다.

여기에 useTransition을 적용해보았다.

SearchInput.tsx

const SearchInput = () => {
  const [keyInput, setKeyInput] = useState('')
  const [transitionKeyword, setTransitionKeyword] = useState(transitionKeywordAtom)
  const setFilterTasks = useSetRecoilState(filterTasksAtom)
  const [, startTransition] = useTransition()
  
  const handleKeyInputChange = (e: ChangeEvent<HTMLInputElement>) => {
    setKeyInput(e.currentTarget.value)
    startTransition(() => setTransitionKeyword(e.currentTarget.value))
  }
    
  useEffect(() => {
    if (filtering.filter) {
     setFilterTasks(filterContents(filtering.type, transitionKeyword, boardsTasks))
    }
  }, [boardsTasks, filtering.filter, filtering.type, setFilterTasks, transitionKeyword])
}

return (
        <input
          value={keyInput}
          onChange={handleKeyInputChange}
        />
            )
  }

여기서, keyInput 말고도 transitionKeyword 라는 startTransition에 적용할 전환 업데이트 state를 따로 생성해 준 이유는 다음과 같다.

useTransition-React-Beta문서

transition은 non-blocking이고, input에 값을 업데이트하는 것은 동기적으로 일어나기 때문이다. 즉, controlled component 의 경우에는 input의 value에 transition을 적용할 수가 없다. 따라서 input 에 값을 입력할 때의 전환 업데이트를 적용하기 위해서는 따로 state를 생성해 주어야 한다.

하지만, 개발자 도구의 performance 탭에서 CPU를 4x 감속하여 테스트 하였을 때 다음과 같이 제대로 적용이 되지 않은 것을 볼 수 있었다.

useEffect에서의 transition 문제

다시 정리하자면, useTransition은 클릭, input 입력과 같은 긴급 업데이트와 데이터를 불러와 정렬하는 등의 전환 업데이트를 구분하여 렌더링에 우선순위를 정할 수 있도록 하는 것이다.

따라서 input에 키워드를 입력하여 transitionKeyword를 전환 업데이트로 변경한다고 하여도, useEffect에서의 로직은 렌더링 이후에 일어나기 때문에 우선 순위 적용이 잘 되지 않는다.

사실, useEffect에는 timeout, eventListener 등의 UI렌더링과는 직접적인 관련이 없는 sideEffect를 넣는 것이 적합하기 때문에 리팩토링을 하면서 useEffect 내의 로직을 바깥으로 빼야겠다고 생각하였다.

useDeferredValue

useDeferredValue는 useTransition과 비슷하지만, setState과 같이 상태를 직접적으로 set해주지 못하는 상태일 때 사용할 수가 있다. 예를 들어, 부모 컴포넌트로 부터 props를 통해 받아왔고, 이 props가 transition이라는 것을 알릴 때 사용할 수가 있다.

useDeferredValue 적용

useEffect 안에 있던 filterContent 함수 로직을 빼서 Board.tsx 컴포넌트로 이동 시키면서 useDeferredValue를 사용해 줄 수가 있었다.

useDeferredValue 적용 코드

Board.tsx

const Board = () => {
  const keyInput = useRecoilValue(keyInputAtom)
  const defferedKeyword = useDeferredValue(keyInput)
    
  const processTasks = filtering.filter
    ? filterContents(filtering.type, defferedKeyword, boardsTasks)[process]
    : boardsTasks[process]
  
  return (
     <ul>
       {processTasks?.map((cardTask: ITask, iCard) => {
           const cardKey = `card=${iCard}`
           return <BoardCard key={cardKey} cardTask={cardTask} index={iCard} />
       })}
    </ul>
    )
  }

SearchInput.tsx

const SearchInput = () => {
  const [keyInput, setKeyInput] = useRecoilState(keyInputAtom)
  const setFilterTasks = useSetRecoilState(filterTasksAtom)
  
  const handleKeyInputChange = (e: ChangeEvent<HTMLInputElement>) => {
    setKeyInput(e.currentTarget.value)
  }

return (
        <input
          value={keyInput}
          onChange={handleKeyInputChange}
        />
       )
  }

결과적으로, 다음과 같이 성공적으로 transition을 적용할 수 있었다.

profile
개발하면서 행복하기
post-custom-banner

0개의 댓글