World-map-note는 보드에 일정 관리와 필터링을 할 수 있는 카드형 대시보드이다.
디자인은 깃헙을 참고하였다.
속도가 빠른 데스크톱 환경에서는 문제가 없었던 카드 필터링 기능이, 모바일 환경에서는 속도가 느렸다. 인풋창에 키워드가 입력되는 속도가 느리고 카드가 필터링 되어 UI로 보여지는데 자꾸 끊겼다.
이럴 때 보통 디바운싱이나 쓰로틀링 기법을 이용해서 일정 시간이 지날 경우 중간 중간 렌더링을 해줄 수 있도록 할 수 있다. 하지만 이는 근본적인 해결법은 아니다.
하지만 이번 react18 부터는 cocurrent 기능을 이용하여 긴급한 동작(버튼 클릭 등)과 그렇지 않은 동작(데이터를 불러와 띄워주는) 을 제어할 수 있게 되었다. Concurrent는 더욱 긴급한 업데이트가 이미 시작한 렌더링을 중단 할 수 있음을 의미한다.
다시 말해서, 긴급 업데이트는 타이핑, 버튼 클릭 등 즉각적으로 상태가 변화하기를 기대하는 업데이트이며, 전환 업데이트는 타이핑 시 연관 검색어 보여주기처럼 즉각적으로 업데이트가 되지 않아도 어색하지 않은 것이다.
이 때 긴급 업데이트는 전환 업데이트에 영향을 받으면 안되기 때문에 useTransition
으로 전환 업데이트를 감싸주어 긴급 업데이트를 우선적으로 해줄 수 있도록 한다.
import { useTransition } from 'react';
function TabContainer() {
const [isPending, startTransition] = useTransition();
// ...
}
useTransition은 위와 같이 사용해줄 수가 있다.
- useTranstion은 아무런 인자도 받지 않는다.
- useTransition은
isPending
과startTransition
두 가지 요소의 배열을 리턴한다.isPending
은 pending transition 상태가 있는 경우 알려주는 역할을 한다.startTransition
은 전환으로서의 상태 업데이트를 마크하는 역할을 한다.
적용 전 기존의 코드는 다음과 같다.
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를 따로 생성해 준 이유는 다음과 같다.
transition은 non-blocking이고, input에 값을 업데이트하는 것은 동기적으로 일어나기 때문이다. 즉, controlled component 의 경우에는 input의 value에 transition을 적용할 수가 없다. 따라서 input 에 값을 입력할 때의 전환 업데이트를 적용하기 위해서는 따로 state를 생성해 주어야 한다.
하지만, 개발자 도구의 performance 탭에서 CPU를 4x 감속하여 테스트 하였을 때 다음과 같이 제대로 적용이 되지 않은 것을 볼 수 있었다.
다시 정리하자면, useTransition은 클릭, input 입력과 같은 긴급 업데이트
와 데이터를 불러와 정렬하는 등의 전환 업데이트
를 구분하여 렌더링
에 우선순위를 정할 수 있도록 하는 것이다.
따라서 input에 키워드를 입력하여 transitionKeyword를 전환 업데이트로 변경한다고 하여도, useEffect에서의 로직은 렌더링 이후
에 일어나기 때문에 우선 순위 적용이 잘 되지 않는다.
사실, useEffect에는 timeout, eventListener 등의 UI렌더링과는 직접적인 관련이 없는 sideEffect를 넣는 것이 적합하기 때문에 리팩토링을 하면서 useEffect 내의 로직을 바깥으로 빼야겠다고 생각하였다.
useDeferredValue
는 useTransition과 비슷하지만, setState과 같이 상태를 직접적으로 set해주지 못하는 상태일 때 사용할 수가 있다. 예를 들어, 부모 컴포넌트로 부터 props를 통해 받아왔고, 이 props가 transition이라는 것을 알릴 때 사용할 수가 있다.
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을 적용할 수 있었다.