때때로, Prop의 변화를 일부 상태에만 반영하고 싶을 때가 있습니다. 예를 들어, List
컴포넌트는 items
를 prop으로 받으며, selection
상태 변수로 선택된 아이템을 관리합니다. items
prop이 변경될 때마다, selection
을 null
로 초기화해야 한다고 가정합시다.
interface Props {
items: Array<Item>;
}
function List({ items }: Props) {
const [isReverse, setIsReverse] = useState<boolean>(false);
const [selection, setSelection] = useState<Item | null>(null);
useEffect(() => {
setSelection(null);
}, [items]);
// ...
}
위 방법은 매우 비효율적입니다. 부모로부터 변경된 items
를 prop으로 받게 되면 어떤 일들이 일어날까요?
List
컴포넌트와 그 자식 컴포넌트들이 렌더됩니다.useEffect
가 실행되며, setSelection(null)
가 호출됩니다.useEffect
를 제거하고, 렌더링 도중에 상태를 바로 변경해봅시다.
const [isReverse, setIsReverse] = useState<boolean>(false);
const [selection, setSelection] = useState<Item | null>(null);
const [prevItems, setPrevItems] = useState<Array<Item>>(items);
if (items !== prevItems) {
setPrevItems(items);
setSelection(null);
}
이전 상태 값을 따로 저장하여 사용하면 이해하기 힘든 코드가 될 수 있지만, useEffect
를 사용하는 것보다 좋습니다. 위 코드를 사용하여 변경된 items
를 받는다면 어떤 일들이 일어날까요?
List
컴포넌트 함수가 종료된 직후 바로 리렌더가 일어납니다. 때문에 자식들은 렌더되지 않으며, DOM 업데이트 또한 일어나지 않습니다.List
컴포넌트 리렌더 되며, 이번엔 자식 컴포넌트까지 렌더됩니다.처음 패턴과 비교했을 때, 자식 컴포넌트 렌더링 1번, DOM 업데이트 1번을 단축할 수 있습니다. 이 패턴은 useEffect
를 사용하는 것보다는 효율적이지만, 사실 이것조차 쓰지 않는 게 좋습니다. 어떤 방법을 사용하건, props나 다른 상태 값의 변화에 따라 일부 상태만을 조정하면, 데이터의 흐름이 이해하기 어려워지고 디버깅이 힘들어집니다. key
를 사용해서 모든 상태를 초기화하거나, 렌더링 도중에 계산해서 사용할 수 있는지를 먼저 고려해 봐야 합니다.
예를 들어, Item
대신에 item의 ID를 저장하면 어떨까요? 선택된 ID가 새로운 items
에 존재한다면, 렌더링 과정에서 이를 찾아 사용하면 됩니다.만약 존재하지 않는다면 null
을 적용하면 됩니다. 처음과 동작이 동일하지 않은 것은 맞지만, 더욱 바람직한 동작인 것은 틀림 없습니다.
const [isReverse, setIsReverse] = useState<boolean>(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const selection = items.find(item => item.id === selectedId) ?? null;
// ...