[배우는 중] 이 useEffect, 사실 필요 없었다
검색 결과를 상태에 담고 useEffect로 다시 계산했더니, 한 박자 늦은 화면이 깜빡였다. effect가 "그 다음 단계"가 아니라는 걸 그때 알았다.
배우는 중(Learning log) — 완성된 정리가 아니라, 혼자 공부하다 막힌 지점과 그걸 넘은 과정을 그대로 남기는 시리즈입니다. 지금 제 이해라서 틀릴 수 있고, 나중에 알게 되면 정정 노트를 답니다.
검색창이 달린 목록을 만들고 있었어요.
입력창에 타이핑하면 아래 목록이 걸러지는, 흔한 화면이요.
제가 처음 짠 건 이랬어요.
원본 목록 items가 있고, 화면에 뿌릴 filtered를 useState로 따로 뒀어요.
그리고 검색어 query가 바뀔 때마다 filtered를 다시 계산하려고 useEffect를 붙였죠.
const [filtered, setFiltered] = useState(items);
useEffect(() => {
setFiltered(items.filter((it) => it.name.includes(query)));
}, [query, items]);
돌아는 갔어요.
그런데 타이핑을 좀 빠르게 하면 목록이 한 박자 늦게 따라오면서 미묘하게 깜빡였어요.
이전 검색 결과가 잠깐 보였다가 바뀌는 거예요.
원인을 한참 뒤에야 알았어요.
렌더가 두 번 도는 거였어요.
query가 바뀌면 컴포넌트가 먼저 옛 filtered로 한 번 그리고, 그다음 effect가 실행돼 setFiltered가 불리면서 다시 그려요.
그 사이의 한 프레임이 깜빡임이었어요.
여기서 이상하다는 걸 느꼈어요.
나는 그냥 “query로 items를 거른 것”을 보여주고 싶을 뿐인데, 왜 상태가 두 개(items와 filtered)나 필요하지?
filtered는 사실 items와 query만 있으면 언제든 다시 만들 수 있는 값이잖아요.
예전에 파생 상태 글에서 겪었던 거랑 똑같은 실수였어요 — 계산해서 나오는 값을 굳이 상태에 담아둔 거요.
그래서 filtered 상태도, 그걸 채우던 effect도 통째로 지웠어요.
렌더 중에 그냥 계산하면 되더라고요.
const filtered = items.filter((it) => it.name.includes(query));
깜빡임이 사라졌어요.
상태가 하나 줄었고, effect도 하나 줄었어요.
query가 바뀌면 컴포넌트가 다시 그려지고, 그리는 김에 filtered도 그 자리에서 새로 계산돼요.
한 박자 늦을 틈이 없어요.
왜 내가 자꾸 effect부터 붙였을까, 를 생각해봤어요.
퍼블리셔로 일할 땐 거의 모든 게 “이벤트가 일어나면 → 그 다음에 이걸 한다”였어요.
클릭하면 클래스 토글, 스크롤하면 헤더 붙이기.
순서대로 벌어지는 일들이요.
그 습관 때문에 useEffect를 “상태가 바뀌면 그 다음에 실행되는 단계”로 읽었던 거예요.
실제론 그런 뜻이 아닌데도요.
리액트 문서를 다시 읽어보니 effect의 쓰임은 생각보다 좁았어요.
effect는 리액트 바깥 세계와 동기화할 때 쓰는 거였어요.
네트워크 구독, 브라우저 API, 리액트가 모르는 외부 위젯 같은 것들.
화면에 보일 값을 state로부터 계산하는 일은 바깥 세계가 아니라 렌더링의 일이고요.
이 기준으로 보니 그동안 붙인 effect 중에 안 써도 되는 게 꽤 많았어요.
부모에게 받은 값으로 무언가를 계산해 state에 넣던 effect — 렌더 중 계산으로 대체돼요.
prop이 바뀔 때 state를 초기화하려고 쓰던 effect — 그건 key로 컴포넌트를 새로 마운트시키는 게 더 깔끔하고요.
버튼을 눌렀을 때 벌어질 일을 effect로 처리하던 것 — 그건 그냥 이벤트 핸들러 안에서 하면 돼요.
사용자가 한 행동은 이미 “언제”가 정해져 있으니 effect로 감시할 이유가 없거든요.
물론 진짜 effect가 맞는 것도 있어요.
서버에서 데이터를 받아오는 건 바깥 세계와의 동기화가 맞아요.
다만 그건 effect로 시작하더라도 결국 서버 상태 도구에 맡기는 게 나았고요.
아직 헷갈리는 지점도 남아 있어요.
”렌더 중에 계산하면 매번 다시 도는데, 무거운 계산은 괜찮나?”인데 — 그건 useMemo가 있고, 예전에 그걸 남용했다 혼난 적이 있어서 이번엔 목록이 정말 커지기 전엔 안 붙이려고요.
지금 이 목록은 수십 개라 그냥 매번 걸러도 티가 안 나요.
그래서 요즘은 effect를 쓰기 전에 한 번 멈춰서 물어봐요.
이게 바깥 세계랑 맞추는 일인가, 아니면 그냥 지금 값으로 계산하면 되는 일인가.
대부분은 후자였어요.
댓글