모든 강의를 무료로 볼 수 있어요. 회원가입 없이도 학습 가능합니다.

11강 / 전체 30강

useEffect 기초

6분 읽기 조회 4

useEffect의 동작 원리와 실행 시점, 의존성 배열의 역할, 클린업 함수가 왜 필요한지를 구체적인 예시로 깊이 이해합니다.

🔄 사이드 이펙트란 무엇인가

리액트 컴포넌트의 주된 역할은 데이터(state, props)를 받아 JSX를 반환하는 것입니다. 그런데 실제 앱에서는 컴포넌트가 렌더링 이외의 작업도 해야 합니다. 예를 들면:

  • 서버에서 데이터 가져오기 (fetch API, axios)
  • DOM 직접 조작 (스크롤 위치, 포커스)
  • 타이머 설정 (setTimeout, setInterval)
  • 이벤트 리스너 등록/해제
  • 로컬 스토리지 읽기/쓰기

이처럼 렌더링과 관계없이 "외부 세계"와 상호작용하는 코드를 사이드 이펙트(Side Effect)라고 합니다. useEffect는 이런 사이드 이펙트를 안전하게 처리하기 위한 훅입니다.

⚙️ useEffect 기본 문법과 실행 시점

import { useEffect, useState } from 'react'

function Timer() {
  const [seconds, setSeconds] = useState(0)

  useEffect(() => {
    console.log('이 코드는 렌더링 후 실행됩니다')
  })

  return <p>{seconds}초</p>
}

useEffect는 컴포넌트가 렌더링된 이후에 실행됩니다. 첫 번째 인수는 실행할 함수(이펙트 함수), 두 번째 인수는 의존성 배열입니다.

📦 의존성 배열 — 세 가지 형태

의존성 배열([])이 언제 실행될지를 제어합니다:

// 1. 배열 없음 — 매 렌더마다 실행 (거의 사용 안 함)
useEffect(() => {
  console.log('매번 실행')
})

// 2. 빈 배열 — 마운트 시 한 번만 실행
useEffect(() => {
  console.log('컴포넌트가 처음 화면에 나타날 때만 실행')
  // API 호출, 이벤트 리스너 등록 등
}, [])

// 3. 값이 있는 배열 — 그 값이 바뀔 때마다 실행
useEffect(() => {
  console.log(`userId가 ${userId}로 바뀌었을 때 실행`)
  // userId에 해당하는 데이터 재패치
}, [userId])

의존성 배열에 있는 값이 이전 렌더의 값과 다를 때 이펙트가 다시 실행됩니다. []는 의존성이 없으니 처음 한 번만 실행됩니다.

🧹 클린업 함수 — 뒷정리

이펙트 함수에서 함수를 반환하면, 그것이 클린업(cleanup) 함수입니다. 컴포넌트가 언마운트되거나 이펙트가 다시 실행되기 직전에 호출됩니다. 이것이 없으면 메모리 누수(memory leak)가 발생합니다.

function Stopwatch() {
  const [time, setTime] = useState(0)
  const [running, setRunning] = useState(false)

  useEffect(() => {
    if (!running) return

    const interval = setInterval(() => {
      setTime((prev) => prev + 1)
    }, 1000)

    // 클린업: 다음 이펙트 실행 전 또는 언마운트 시 인터벌 제거
    return () => {
      clearInterval(interval)
    }
  }, [running])  // running이 바뀔 때마다 실행

  return (
    <div>
      <p>{time}초</p>
      <button onClick={() => setRunning(!running)}>{running ? '정지' : '시작'}</button>
    </div>
  )
}

클린업이 없다면 running이 바뀔 때마다 새 인터벌이 추가되어 시간이 빠르게 흐르고, 컴포넌트가 제거된 후에도 인터벌이 계속 동작합니다.

🌐 데이터 패칭과 useEffect

가장 흔한 useEffect 사용 사례는 데이터 패칭입니다:

interface Post { id: number; title: string; body: string }

function PostDetail({ postId }: { postId: number }) {
  const [post, setPost] = useState<Post | null>(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<string | null>(null)

  useEffect(() => {
    setLoading(true)
    setError(null)

    fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`)
      .then((res) => {
        if (!res.ok) throw new Error('서버 오류')
        return res.json()
      })
      .then((data: Post) => setPost(data))
      .catch((e) => setError(e.message))
      .finally(() => setLoading(false))
  }, [postId])  // postId가 바뀔 때마다 재패치

  if (loading) return <div>불러오는 중...</div>
  if (error) return <div>오류: {error}</div>
  if (!post) return null

  return (
    <article>
      <h2>{post.title}</h2>
      <p>{post.body}</p>
    </article>
  )
}

⚠️ 흔한 실수 — 무한 루프

객체나 배열을 의존성 배열에 넣으면 무한 루프가 발생할 수 있습니다. 렌더마다 새 객체/배열이 생성되고, 참조가 다르다고 판단해 이펙트를 계속 실행합니다.

// ❌ 매 렌더마다 새 객체 → 무한 루프
useEffect(() => {
  fetchData(options)
}, [{ page: 1, limit: 10 }])  // 매번 새 객체 참조!

// ✅ 원시값을 의존성으로
useEffect(() => {
  fetchData({ page, limit })
}, [page, limit])

✅ 정리

  • useEffect는 렌더링 이후 사이드 이펙트를 처리합니다.
  • 의존성 배열: 없음(매 렌더) / [](마운트 1회) / [값](값 변경 시).
  • 클린업 함수로 타이머·이벤트 리스너를 해제해 메모리 누수를 방지합니다.
  • 객체/배열을 의존성으로 넣으면 무한 루프 — 원시값으로 분해해서 넣으세요.

관련 주제

  • useEffect 훅
  • 의존성 배열
  • 마운트 업데이트 시점
  • 클린업 함수
  • 사이드 이펙트
  • 개발·프로그래밍
  • 개발·프로그래밍 강의
  • 리액트 입문 — 컴포넌트로 만드는 웹
  • 무료강의
  • 무료 온라인 강의
  • NUGUNA
  • 누구나

📚 시리즈 전체 공유

리액트 입문 — 컴포넌트로 만드는 웹

이 강의가 속한 시리즈는 총 30강, 모두 무료입니다. 처음부터 배우려는 동료에게 시리즈 전체를 알려 주세요.

댓글

0/1000

불러오는 중...