frontend/react

[React] React.js 강좌 30. useEffect 완전 정복 — 렌더링 이후에 일어나는 일들

mirabo01 2025. 11. 11. 08:51

React를 조금 다뤄본 사람이라면 useEffect를 안 쓸 수 없습니다.
API 호출, 이벤트 등록, 타이머 설정, 외부 상태 동기화 등 거의 모든 “부수효과(Side Effect)” 처리가 이 훅에 들어갑니다.
하지만 많은 초보자들이 useEffect를 ‘렌더링 직후 실행되는 함수’ 정도로만 이해하고 있어서,
원치 않는 무한 루프나 타이밍 꼬임을 겪곤 합니다.

이번 글에서는 useEffect가 정확히 언제, 어떻게 동작하는지
React의 렌더링 흐름과 연결해가며 구체적으로 설명하겠습니다.


useEffect는 언제 실행되는가

React 컴포넌트는 다음 순서로 실행됩니다.

  1. 렌더링(Render): 컴포넌트 함수가 실행되어 Virtual DOM 생성
  2. 커밋(Commit): 변경된 부분이 실제 DOM에 반영
  3. 이후에 useEffect 콜백 실행

즉, useEffect는 화면이 실제로 그려진 다음에 실행됩니다.
그래서 DOM을 직접 조작하거나, 외부 라이브러리를 연동할 때 안전하게 쓸 수 있습니다.

useEffect(() => {
  console.log('렌더링 후 실행!');
});

이 코드는 매번 렌더링 직후 실행됩니다.
만약 특정 값이 바뀔 때만 실행되게 하려면 의존성 배열을 사용해야 합니다.

useEffect(() => {
  console.log('count 값이 바뀜:', count);
}, [count]);

의존성 배열이 비어 있으면,
마운트(최초 렌더링)될 때 한 번만 실행됩니다.

useEffect(() => {
  console.log('컴포넌트가 처음 렌더링됨');
}, []);

의존성 배열을 정확히 이해해야 하는 이유

제가 예전에 겪은 일입니다.
API 데이터를 불러오는 코드를 아래처럼 작성했는데,
계속해서 API가 반복 호출되는 현상이 생겼습니다.

useEffect(() => {
  fetchData();
}, [data]);

문제는 fetchData 안에서 setData를 호출했기 때문이었습니다.
data가 바뀌면 useEffect가 다시 실행되고,
다시 setData가 실행되어 무한 루프가 생긴 거죠.

이런 문제를 막으려면,
의존성 배열에 정말 필요한 값만 넣어야 합니다.
또는 fetch 로직을 컴포넌트 바깥으로 분리하거나
useCallback으로 함수 참조를 고정시키는 방법도 있습니다.

const fetchData = useCallback(async () => {
  const res = await axios.get('/api/users');
  setData(res.data);
}, []); // 함수 참조 고정

정리하자면, useEffect는 “렌더 이후의 동기화”다

많은 분들이 useEffect를 “렌더링 직후 실행되는 함수”로만 보지만,
사실은 “렌더링 결과를 외부 세계와 동기화하기 위한 도구”입니다.

즉, 화면에 무언가 바뀐 뒤 → 그 변화에 맞게 API를 호출하거나 → 이벤트를 등록하거나 → 데이터를 갱신하는 식이죠.

React 공식 문서에서도 이를
“side effects — rendering과 직접 관련 없는 일들”로 설명합니다.


useEffect의 정리(clean-up) 함수

useEffect 내부에서 반환한 함수는
다음 렌더링이 일어나기 전, 또는 컴포넌트가 언마운트될 때 실행됩니다.

useEffect(() => {
  const id = setInterval(() => console.log('타이머 동작 중'), 1000);

  return () => clearInterval(id); // 정리(clean-up)
}, []);

이 정리 함수는 “이전 효과를 정리하고 새로운 효과를 실행한다”는 React의 원칙을 반영합니다.
특히 이벤트 리스너나 타이머, 구독(subscription) 같은 코드를 쓸 땐 필수입니다.

이걸 제대로 처리하지 않으면
컴포넌트가 사라진 후에도 이벤트가 계속 호출되는 버그가 생깁니다.


useLayoutEffect와의 차이

간혹 useEffect 대신 useLayoutEffect를 써야 하는 상황도 있습니다.
이 둘은 실행 시점이 다릅니다.

  • useEffect: 브라우저가 화면을 그린 후 실행
  • useLayoutEffect: DOM이 업데이트된 직후, 화면이 그려지기 전에 실행

즉, 시각적 깜빡임(flicker)을 방지하거나 DOM 측정이 필요한 경우엔
useLayoutEffect를 써야 합니다.

useLayoutEffect(() => {
  const { height } = ref.current.getBoundingClientRect();
  setHeight(height);
}, []);

렌더링 직후 DOM 크기를 측정해야 하는 UI에서는
useLayoutEffect가 더 정확합니다.


실무에서 자주 겪는 useEffect 이슈들

1. 의존성 배열 누락

eslint-plugin-react-hooks가 이걸 감지합니다.
무시하지 말고, 경고를 통해 의존성을 명시적으로 맞춰야 합니다.

2. 상태 갱신 시점 문제

useEffect는 “렌더링 이후” 실행되므로,
렌더링 직전에 어떤 상태를 쓰고 싶다면 useMemo나 useLayoutEffect를 고려해야 합니다.

3. SSR(Server-Side Rendering) 환경

Next.js처럼 서버 렌더링을 쓰는 환경에서는
useEffect가 클라이언트에서만 실행됩니다.
서버에서 실행해야 할 초기화 코드는 getServerSideProps 같은 다른 영역에 두어야 합니다.


useEffect를 다루는 요령

React에서 useEffect는 “모든 문제의 원인”이자 “해결책”이기도 합니다.
코드를 작성할 때 다음 세 가지를 꼭 체크하면 대부분의 문제를 피할 수 있습니다.

  1. 이 effect가 렌더링 결과에 의존하는가?
  2. 의존성 배열에 정말 필요한 값만 들어있는가?
  3. 정리(clean-up)가 누락되지 않았는가?

이 세 가지만 지켜도
불필요한 리렌더링, 무한 루프, 메모리 누수 같은 문제를 거의 막을 수 있습니다.


React의 렌더링 흐름을 이해했다면 이제 useEffect는 낯설지 않을 겁니다.
다음 글에서는 이 흐름을 확장해,
React 성능을 실제로 측정하고 분석하는 방법을 다뤄보겠습니다.
“느리다”는 감이 아니라, 수치로 성능을 확인하고 개선하는 과정입니다.