Skip to main content

Command Palette

Search for a command to run...

useEffect 지옥이란 무엇이며, 어떻게 안전하게 사용하는가

Published
3 min read

React를 어느 정도 사용하다 보면 한 번쯤은
“useEffect 지옥에 빠졌다”는 말을 듣거나 직접 느껴본 적이 있을 것이다.

  • useEffect가 계속 늘어나고

  • 의존성 배열은 점점 길어지고

  • 왜 실행되는지 이해하기 어려워지며

  • eslint-disable-next-line 이 늘어나는 상태

하지만 많은 사람들이 오해한다.
useEffect 지옥은 useEffect가 많아서 생기는 문제가 아니다.

이 글에서는

  1. ‘useEffect 지옥’의 정확한 의미

  2. 무엇이 진짜 문제인지

  3. 실무에서 안전하게 사용하는 패턴은 무엇인지
    를 정리한다.


useEffect 지옥의 정확한 의미

useEffect 지옥이란
컴포넌트의 동작 흐름이 state 변화와 effect에 암묵적으로 묶여
코드만 보고는 “언제, 왜 실행되는지” 이해할 수 없는 상태다.

핵심은 개수도 아니고, 의존성 배열의 길이도 아니다.

useEffect(() => subscribe(), [])
useEffect(() => fetchUser(id), [id])
useEffect(() => trackPage(path), [path])

이처럼 effect가 여러 개여도

  • 각 effect의 역할이 명확하고

  • 서로 영향을 주지 않는다면
    전혀 지옥이 아니다.


흔한 오해들

❌ useEffect가 많으면 지옥이다?

아니다.
“이 effect는 무슨 역할인가?”를 한 문장으로 설명할 수 있다면 문제 없다.

❌ 의존성 배열이 길면 지옥이다?

아니다.
문제는 여러 원인 → 여러 부작용을 한 effect에서 처리할 때다.

// ❌ 위험한 패턴
useEffect(() => {
  if (a) setX(...)
  if (b) fetch(...)
  if (c) setY(...)
}, [a, b, c])

이 경우, 실행 이유와 결과가 섞이면서 추론이 어려워진다.


useEffect 지옥의 진짜 원인

1. 파생 상태를 state + effect로 관리할 때

// ❌
useEffect(() => {
  setTotal(price * count)
}, [price, count])

이런 코드는

  • 선언적인 계산을

  • 명령형 사이드 이펙트로 바꿔버린다.

// ✅
const total = price * count

2. 이벤트를 effect로 처리할 때

// ❌
useEffect(() => {
  if (clicked) submit()
}, [clicked])

사용자 액션은 이벤트 핸들러에서 직접 처리해야 한다.
effect는 자동 실행되기 때문에 제어 흐름을 흐리게 만든다.


3. effect들이 서로를 트리거할 때

useEffect(() => {
  setA(...)
}, [b])

useEffect(() => {
  setB(...)
}, [a])

이런 구조에서는 실행 순서가 코드에 드러나지 않는다.
렌더링 사이클을 머릿속에서 시뮬레이션해야 한다.


그럼 useEffect는 언제 “안전한가”?

useEffect는 오직 React 외부 세계와의 동기화에만 사용될 때 안전하다.


안전한 useEffect 사용 예시

1. API 요청 (서버 상태 동기화)

useEffect(() => {
  let cancelled = false

  fetch(`/api/users/${userId}`)
    .then(res => res.json())
    .then(data => {
      if (!cancelled) setUser(data)
    })

  return () => {
    cancelled = true
  }
}, [userId])
  • 입력(userId) ↔ 서버 데이터 동기화

  • effect의 존재 이유가 명확


2. 이벤트 리스너 등록 / 해제

useEffect(() => {
  const onResize = () => setWidth(window.innerWidth)

  window.addEventListener('resize', onResize)
  return () => window.removeEventListener('resize', onResize)
}, [])
  • 브라우저 API는 React 외부 세계

  • cleanup이 명확한 패턴


3. 타이머 관리

useEffect(() => {
  const id = setInterval(() => {
    setTime(new Date())
  }, 1000)

  return () => clearInterval(id)
}, [])
  • 타이머 생명주기 = 컴포넌트 생명주기

  • effect의 역할이 명확


4. WebSocket / 구독 관리

useEffect(() => {
  const socket = new WebSocket(url)

  socket.onmessage = e => {
    setMessages(prev => [...prev, JSON.parse(e.data)])
  }

  return () => socket.close()
}, [url])
  • 연결/해제 관리

  • 외부 리소스 동기화의 전형적인 예


5. DOM 직접 제어

useEffect(() => {
  inputRef.current?.focus()
}, [])
  • 렌더 이후에만 가능한 작업

  • effect가 아니면 해결 불가


안전한 useEffect 판단 체크리스트

다음 질문에 모두 YES면 안전하다.

  1. 이 effect는 React 외부의 무언가를 다루는가?

  2. 이 effect를 제거하면 UI 선언만으로는 불가능한가?

  3. effect 하나에 책임이 하나뿐인가?

  4. 실행 이유를 한 문장으로 설명할 수 있는가?


결론

useEffect 지옥은
useEffect를 ‘동기화 도구’가 아니라
‘제어 흐름 도구’로 사용하면서 시작된다.

  • 개수는 문제가 아니다

  • 의존성 배열도 문제가 아니다

  • 역할이 섞인 effect가 문제다

useEffect는 줄이는 게 목표가 아니라,
쓸 이유가 분명한 곳에만 쓰는 것이 목표다.


🧾 작성 참고

이 글은 ChatGPT의 도움을 받아 내용을 정리하였습니다.

More from this blog

LLM은 어떻게 글을 읽고 쓰는가 — 입력부터 출력까지

대화형 AI에게 질문을 던지면, 마치 사람처럼 문장을 이해하고 답을 써 내려가는 것처럼 보인다.하지만 그 안에서 벌어지는 일은 생각보다 단순하고, 또 생각보다 기계적이다.이 글에서는 두 가지 질문에 답해 본다.LLM은 어떻게 글을 만들어내는가,그리고 우리가 입력한 프롬프트는 모델에 어떤 모습으로 들어가는가. 1부. LLM은 어떻게 글을 만들어내는가 핵심은

Jun 10, 20264 min read

Zod란 무엇인가: TypeScript에서 런타임 검증과 타입 안정성을 동시에 해결하는 방법

TypeScript 프로젝트를 하다 보면 반드시 마주치는 문제가 있다.바로 “타입은 있는데, 데이터는 믿을 수 없다”는 것이다. TypeScript는 컴파일 타임에는 강력하지만,런타임에 들어오는 데이터에 대해서는 아무런 보장을 해주지 않는다. API 요청 데이터 사용자 입력(Form) 환경 변수 (process.env) 외부 JSON / 설정 파일 이 모든 것은 타입 시스템 바깥에서 들어온다. 이 문제를 해결하기 위해 등장한 라이브러...

Jan 10, 20263 min read

Flatpak을 이해하기 위한 배경 지식

배포판, 라이브러리 충돌, 그리고 OSTree까지 리눅스 데스크톱에서 Flatpak이 표준처럼 자리 잡은 데에는 분명한 이유가 있다.그 이유를 이해하려면 단순히 “Flatpak은 패키징 시스템이다”를 넘어서,리눅스 배포판 구조, 라이브러리 버전 충돌, OSTree라는 기술까지 함께 이해해야 한다. 이 글에서는 다음 질문에 답해본다. 리눅스에서 말하는 배포판이란 무엇인가? 라이브러리 버전 충돌은 왜 발생하는가? Flatpak은 이 문제를 어...

Jan 8, 20265 min read

npm SemVer 실무 가이드

이 글은 npm의 Semantic Versioning(SemVer) 을 처음 접하는 개발자부터, 실무에서 이미 사용하고 있지만 헷갈리는 포인트가 많은 개발자까지를 대상으로 한다. 단순한 규칙 나열이 아니라, 실제 업무에서 어떻게 쓰이고, 어디서 사고가 나며, 어떤 관습이 굳어졌는지를 중심으로 정리했다. 1. SemVer란 무엇인가? SemVer(Semantic Versioning)는 버전 번호에 의미를 부여하는 규칙이다. 기본 형식은 다음과 ...

Jan 7, 20263 min read

Dev note

31 posts