본문 바로가기

Dev Log

useEffect 꿀팁

728x90

모방은 창조의 어머니라고 하지 않았던가. 코딩 설계도 다 모방에서 시작되는 법. 최근에 재밌게 읽은 글이 있는데

Start naming your useEffect functions, you will thank me later

이 블로그였다. 뭐랄까, thank me later할 정도로 자신 넘치는 애티튜드가 느껴졌달까? useEffect 팁(내가 해주는 팁은 아니지만)을 정리해보려고 한다.

 

근데 왜 useEffect가 읽기 어렵냐면

저자 Dan이 동료 PR을 열었는데, 200줄짜리 컴포넌트에 useEffect가 4개 있었다고 한다. 코드 자체는 잘 짜여 있었는데, 각 effect가 뭘 하는지 파악하려고 한 줄 한 줄 다 읽어야 했다는 것.

useEffect(() => { 이 시작 문장은 언제 실행되는지는 알려주지만, 실행되는지는 전혀 말해주지 않기 때문이다.

 

해결책은 단순하다 — 이름을 붙여라

// 기존 방식
useEffect(() => {
  document.title = `${count} items`;
}, [count]);

// 이름 붙인 버전
useEffect(function updateDocumentTitle() {
  document.title = `${count} items`;
}, [count]);

문법 딱 하나 바꾼 것뿐인데, effect가 4개 있는 컴포넌트라면 이름만 죽 훑어도 전체 데이터 흐름이 한눈에 들어온다.

useEffect(function connectToInventoryWebSocket() { ... })
useEffect(function fetchInitialStock() { ... })
useEffect(function resetStockOnLocationChange() { ... })
useEffect(function notifyParentOfStockUpdate() { ... })

코드 한 줄도 안 읽고도 이 컴포넌트가 뭘 하는지 파악 완료.

 

디버깅할 때도 빛을 발한다

익명 함수일 때 에러 나면 콘솔에 이렇게 뜬다:

at (anonymous) @ InventorySync.tsx:14

 

effect가 4개면 어디서 터진 건지 알 수가 없다. 이름을 붙이면?

at connectToInventoryWebSocket @ InventorySync.tsx:14

Sentry 같은 모니터링 툴에서 폰으로 에러 확인할 때 특히 차이가 크다.

 

이름을 못 짓겠다면, 그게 신호다

이름 붙이려다 보면 이런 상황이 온다:

useEffect(() => {
  const handleResize = () => setWidth(window.innerWidth);
  window.addEventListener('resize', handleResize);

  if (user?.preferences?.theme) {
    document.body.className = user.preferences.theme;
  }

  return () => window.removeEventListener('resize', handleResize);
}, [user?.preferences?.theme]);

 

이걸 뭐라고 부르지? syncWidthAndApplyTheme? "and"가 들어가는 순간 경고 신호다. 관련 없는 두 가지 일을 하나의 effect에 욱여넣고 있다는 뜻이니까, 그냥 쪼개면 된다.

useEffect(function trackWindowWidth() {
  const handleResize = () => setWidth(window.innerWidth);
  window.addEventListener('resize', handleResize);
  return () => window.removeEventListener('resize', handleResize);
}, []);

useEffect(function applyUserTheme() {
  if (user?.preferences?.theme) {
    document.body.className = user.preferences.theme;
  }
}, [user?.preferences?.theme]);

 

심지어 "이 effect 자체가 필요 없다"는 것도 알 수 있다

useEffect(function syncFullName() {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);

 

이름 붙이고 보니... 이거 그냥 derived value잖아?

const fullName = `${firstName} ${lastName}`;

effect 버전은 렌더링을 두 번 일으킨다. 이름을 붙이는 행위 자체가 "이 코드가 effect에 있을 필요가 있나?"를 다시 생각하게 만드는 것이다.

 

Dan의 실제 경험담

1년 전에 Mapbox 인스턴스를 앱 상태와 동기화하는 컴포넌트를 작업했는데, effect가 5개였다고 한다. 파일 열 때마다 30초씩 다시 파악해야 했던 것.

이름을 붙였더니:

  • cleanupStaleMarkerListeners가 사실 handleMarkerInteractions의 cleanup이었다는 걸 발견 → 합침
  • synchronizeZoomLevel이랑 synchronizeCenterPosition이 항상 같이 실행된다는 걸 발견 → synchronizeMapViewport로 합침

effect 5개가 3개로 줄었다. 이름이 구조를 보이게 만든 것이다.

 

cleanup 함수도 이름 붙일 수 있다

useEffect(function pollServerForUpdates() {
  const intervalId = setInterval(() => {
    fetch(`/api/status/${serverId}`)
      .then(res => res.json())
      .then(setServerStatus);
  }, 5000);

  return function stopPollingServer() {
    clearInterval(intervalId);
  };
}, [serverId]);

 

pollServerForUpdates로 시작해서 stopPollingServer로 끝나는 대칭 구조. 읽으면서 자연스럽게 흐름이 잡힌다.

코드 한 글자도 안 바꾸고, 라이브러리도 필요 없고, 그냥 이름 하나 붙이는 것뿐인데. 앞으로 useEffect 쓸 때마다 이름 꼭 붙여봐야겠다.

 

! 요약 !

useEffect에 이름을 붙이면 

  1. 에러 추적이 쉬워진다. 에러가 나면 (anonymous) 대신 connectToInventoryWebSocket이 뜬다.
  2. 자연히 클린코드를 적게된다. 이름을 못 짓겠으면 그 effect가 너무 많은 일을 하고 있다는 신호다.
  3. cleanup 함수 시작과 끝이 한눈에 보인다.
728x90