값 하나 합치려고 state와 Effect를 더 만들었다가 렌더가 두 번 돈 이유
문제 발생
이름 두 개를 합친 값을 화면에 쓰려고 state와 effect를 하나씩 더 만들었습니다.
const [firstName, setFirstName] = useState("Taylor");
const [lastName, setLastName] = useState("Swift");
const [fullName, setFullName] = useState("");
useEffect(() => {
setFullName(firstName + " " + lastName);
}, [firstName, lastName]);타이핑할 때마다 렌더가 두 번씩 일어났고, 한 프레임 동안은 옛 값이 보였습니다.
원인 분석
React 공식 문서가 이 패턴을 불필요한 state와 불필요한 Effect로 지목합니다. 동작 순서를 보면 낭비가 분명합니다 — React가 옛 fullName으로 한 번 렌더하고, 그다음 Effect가 실행되고, setFullName 때문에 다시 렌더합니다. 화면에 필요한 값 하나 때문에 렌더 패스가 한 번 더 생깁니다.
문서의 원칙은 간단합니다 — 기존 props나 state로 계산할 수 있는 것은 state에 두지 마십시오. 렌더링 중에 계산하십시오. 그러면 연쇄적인 갱신이 사라져 더 빠르고, 단순하고, 실수할 여지가 줄어듭니다.
같은 형태의 함정이 하나 더 있습니다 — prop이 바뀔 때 state를 초기화하려고 Effect를 쓰는 것입니다. 이 역시 옛 값으로 한 번 렌더한 뒤에야 갱신됩니다.
해결 방안
- 파생 값은 그냥 계산합니다.
const [firstName, setFirstName] = useState("Taylor");
const [lastName, setLastName] = useState("Swift");
const fullName = firstName + " " + lastName; // state도 Effect도 필요 없음- prop이 바뀔 때 상태를 초기화하려면
key를 씁니다. 문서가 가장 좋은 방법으로 제시하는 형태입니다. 컴포넌트를 새로 마운트시켜 내부 state가 자동으로 초기화됩니다.
export default function ProfilePage({ userId }) {
return <Profile userId={userId} key={userId} />;
}
function Profile({ userId }) {
const [comment, setComment] = useState(""); // key가 바뀌면 자동 초기화
}- 일부만 조정해야 하면 렌더 중에 조정합니다. Effect보다 낫지만, 문서도 대부분의 컴포넌트에는 이것조차 필요 없다고 덧붙입니다.
function List({ items }) {
const [selection, setSelection] = useState(null);
const [prevItems, setPrevItems] = useState(items);
if (items !== prevItems) {
setPrevItems(items);
setSelection(null);
}
}- 제일 좋은 것은 조정할 state를 아예 없애는 것입니다. id만 들고 있으면 조정이 필요 없습니다.
function List({ items }) {
const [selectedId, setSelectedId] = useState(null);
const selection = items.find((item) => item.id === selectedId) ?? null;
}- 비싼 계산은 Effect가 아니라
useMemo입니다.
const visibleTodos = useMemo(
() => getFilteredTodos(todos, filter),
[todos, filter],
);- 비싼지 먼저 재봅니다. 문서가 제시하는 기준은 실측입니다 —
console.time()으로 재서 합계가 1ms 이상이면 메모이제이션할 가치가 있을 수 있습니다.
console.time("filter array");
const visibleTodos = getFilteredTodos(todos, filter);
console.timeEnd("filter array");- React Compiler를 쓰면 상당수는 자동입니다. 문서도 컴파일러가 비싼 계산을 자동으로 메모이제이션해 수동
useMemo가 필요 없어지는 경우가 많다고 적습니다. 다만 1~4번의 "state를 만들지 않는다"는 판단은 컴파일러가 대신해 주지 않습니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.