티스토리 뷰

 

리액트의 함수형 컴포넌트에서 **사이드 이펙트(Side Effects)**를 처리하기 위해 사용하는 useEffect는 가장 강력하면서도 오용하기 쉬운 훅(Hook)입니다. 단순히 "생명주기 메서드(Lifecycle methods)의 대체제"라고 생각하면 예기치 못한 버그를 마주하기 쉽습니다.

오늘은 useEffect의 핵심 원리와 실무 예제, 그리고 시니어 개발자들도 가끔 저지르는 흔한 실수들을 깊이 있게 살펴보겠습니다.

1. useEffect란 무엇인가?

useEffect는 컴포넌트가 렌더링된 이후에 외부 시스템(API, 타이머, DOM 직접 조작 등)과 동기화할 수 있게 해주는 훅입니다.

기본 문법

useEffect(() => {
  // 실행할 코드 (Side Effect)

  return () => {
    // 정리 코드 (Cleanup)
  };
}, [dependencies]); // 의존성 배열

2. 의존성 배열(Dependency Array)에 따른 동작

의존성 배열을 어떻게 설정하느냐에 따라 실행 시점이 결정됩니다.

  1. 배열이 없을 때: 매 렌더링마다 실행됩니다. (성능 저하의 원인이 될 수 있음)
  2. 빈 배열 []: 컴포넌트가 처음 나타날 때(Mount) 딱 한 번만 실행됩니다.
  3. 값이 있을 때 [count]: 컴포넌트가 처음 나타날 때와 count 값이 변경될 때마다 실행됩니다.

3. 실무 예제로 배우는 올바른 사용법

예제 1: API 데이터 페칭 (기본)

컴포넌트가 마운트될 때 데이터를 가져오는 전형적인 사례입니다.

import { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    let isIgnore = false; // 클린업을 위한 플래그

    async function fetchUser() {
      const response = await fetch(`https://api.example.com/users/${userId}`);
      const data = await response.json();
      if (!isIgnore) {
        setUser(data);
      }
    }

    fetchUser();

    return () => {
      isIgnore = true; // 언마운트 시 혹은 id 변경 시 이전 요청 결과 무시
    };
  }, [userId]); // userId가 바뀔 때마다 다시 페칭

  if (!user) return
로딩 중...
;
  return
{user.name} 님 안녕하세요!
;
}

예제 2: 이벤트 리스너와 클린업(Cleanup)

메모리 누수를 방지하기 위해 구독을 해제하는 과정이 필수적입니다.

useEffect(() => {
  const handleResize = () => {
    console.log('창 크기 변경됨:', window.innerWidth);
  };

  window.addEventListener('resize', handleResize);

  // 컴포넌트가 사라지기 전이나 다음 Effect가 실행되기 전에 호출됨
  return () => {
    window.removeEventListener('resize', handleResize);
  };
}, []);

4. 흔히 하는 실수들 (Mistakes)

실수 1: 의존성 배열 빠뜨리기 (Linter 경고 무시)

Effect 안에서 사용하는 상태나 프롭스가 의존성 배열에 없으면, 해당 값은 '과거의 값(Stale closure)'을 참조하게 됩니다.

// ❌ 나쁜 예
useEffect(() => {
  console.log(count); 
}, []); // count가 변해도 이 Effect는 다시 실행되지 않음 (초기값만 출력)

// ✅ 좋은 예
useEffect(() => {
  console.log(count);
}, [count]);

실수 2: 불필요한 Effect 사용 (상태 동기화)

한 상태를 바탕으로 다른 상태를 업데이트할 때 useEffect를 쓰는 경우가 많지만, 이는 불필요한 추가 렌더링을 유발합니다.

// ❌ 나쁜 예: 상태 변경을 감지해서 다른 상태를 변경
const [firstName, setFirstName] = useState('Gildong');
const [lastName, setLastName] = useState('Hong');
const [fullName, setFullName] = useState('');

useEffect(() => {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);

// ✅ 좋은 예: 렌더링 과정에서 직접 계산
const fullName = `${firstName} ${lastName}`;

실수 3: 무한 루프에 빠지는 경우

Effect 안에서 의존성 배열에 포함된 상태를 직접 업데이트하면 무한 루프가 발생합니다.

// ❌ 무한 루프 발생
useEffect(() => {
  setCount(count + 1);
}, [count]);

// ✅ 해결책: 함수형 업데이트 사용 (의존성 제거 가능)
useEffect(() => {
  setCount(prev => prev + 1);
}, []); // 의존성 배열에서 count를 제거할 수 있음

실수 4: 객체나 함수를 의존성 배열에 넣기

자바스크립트에서 객체나 함수는 참조 타입입니다. 컴포넌트가 리렌더링될 때마다 새로운 참조값이 생성되므로, 내용이 같아도 useEffect는 이를 "변경됨"으로 인식합니다.

// ❌ 매번 리렌더링될 때마다 Effect 실행
const options = { color: 'blue' }; 

useEffect(() => {
  console.log('색상 변경됨');
}, [options]); 

// ✅ 해결책: useMemo를 사용해 참조 유지
const options = useMemo(() => ({ color: 'blue' }), []);

useEffect(() => {
  console.log('색상 변경됨');
}, [options]);

5. 결론: useEffect를 대하는 마음가짐

useEffect는 단순히 생명주기에 맞춰 코드를 끼워 넣는 도구가 아닙니다. **"현재의 프롭스와 상태를 외부 시스템과 어떻게 동기화할 것인가?"**를 정의하는 선언적인 도구입니다.

  • 불필요한 Effect는 줄이세요. (렌더링 중에 계산 가능한 것은 렌더링 중에 하세요)
  • 의존성 배열은 정직하게 작성하세요.
  • 클린업 함수를 작성하는 습관을 들이세요.

이 원칙들만 지켜도 리액트 애플리케이션의 성능과 예측 가능성이 훨씬 높아질 것입니다.

반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/04   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30
글 보관함