본문으로 건너뛰기
yceffort
PostsSeriesTagsAbout🧪 Research
EN

Tweaks

theme
accent palette
film grain
minimal mode
BACK TO INDEX
◆ ESSAY
--min
--year
KOoriginal

mailMail icongithubtwitter
yceffort
•
© 2026
•
https://yceffort.kr
BACK TO INDEX
◆ ESSAY

useEffect는 라이프 사이클 메소드가 아니다.

avatar
yceffort
2020-10-02 · 4분
4min
2020year
KOoriginal
react

useEffect는 라이프 사이클 메소드가 아니다

과거 리액트 클래스 컴포넌트에는 constructor componentDidMount componentDidUpdate componentWillUnmount 와 같이 리액트 라이프 사이클에 대응할 수 있는 각각의 메소드가 존재했다. 함수형 컴포넌트의 훅으로 넘어오면서, 이러한 라이프 사이클 메소드를 훅으로 각각 대체하려고 하지만 이는 큰 실수다.

결론부터 말하자면, useEffect는 라이프 사이클 훅이 아니다. useEffect는 app 의 state값을 활용하여 동기적으로 부수효과를 만들 수 있는 메커니즘이다.

The question is not "when does this effect run" the question is "with which state does this effect synchronize with"

https://twitter.com/ryanflorence/status/1125041041063665666

useEffect(fn) // all state
useEffect(fn, []) // no state
useEffect(fn, [these, states])

eslint-plugin-react-hooks/exhaustive-deps로 deps를 무시하지마라

물론 기술적으로는 가능하다. 그리고 때로는 사용하는게 좋은 이유가 될 수 있다. 그러나 대부분의 경우에 이를 사용하는 것은 나쁜 생각이며, 잠재적인 버그를 만들 수 있다. 이런 이야기를 했을 때, 많은 사람들이 '컴포넌트를 mount 했을 때만 실행 하고 싶을 때가 있다' 라고 말할 수 있다. 그러나 이는 라이프사이클의 접근법이며, 옳지 못하다. useEffect에 deps가 있을 경우, effect 백은 deps에 변화가 있을 때 항상 실행된다. 그 외에는, app의 state 변화로 부터 부수효과와 분리되어 sync가 맞지 않게 된다. (app의 state값과 부수효과가 별개로 돌아가게 된다.)

요약하자면, 이는 버그로 이어질 수 있으므로 해당 룰을 off해서는 안된다.

app의 state와 상관없이 mount시에 실행되어야만 하는 코드가 얼마나 많겠느냐? 하는 의미로 받아드리면 될 것 같다.

하나의 큰 useEffect를 만들지 마라.

각각의 useEffect는 관심사를 따로 분리해 두어야 한다. 하나의 큰 useEffect보다는, 각각의 로직을 분리해두는 것이 훨씬 좋다.

불필요한 외부 함수를 만들지 마라.

아래와 같은 코드는, useEffect에 두가지 deps를 추가해야 된다.

// before. Don't do this!
function DogInfo({dogId}) {
  const [dog, setDog] = React.useState(null)
  const controllerRef = React.useRef(null)
  const fetchDog = React.useCallback((dogId) => {
    controllerRef.current?.abort()
    controllerRef.current = new AbortController()
    return getDog(dogId, {signal: controller.signal}).then(
      (d) => setDog(d),
      (error) => {
        // handle the error
      },
    )
  }, [])
  React.useEffect(() => {
    fetchDog(dogId)
    return () => controller.current?.abort()
  }, [dogId, fetchDog])
  return <div>{/* render dog's info */}</div>
}

위의 코드를 다음과 같이 바꿨다.

function DogInfo({dogId}) {
  const [dog, setDog] = React.useState(null)
  React.useEffect(() => {
    const controller = new AbortController()
    getDog(dogId, {signal: controller.signal}).then(
      (d) => setDog(d),
      (error) => {
        // handle the error
      },
    )
    return () => controller.abort()
  }, [dogId])
  return <div>{/* render dog's info */}</div>
}

useEffect 밖에서 정의되어 있던 fetchDog 함수를 useEffect 내부로 가지고 왔다. 이전에는 이것이 외부에 정의되어 있었기 때문에, deps 배열에 추가해야 했다. 또한 이 때문에 무한 루프에 빠지는 것을 방지하기 위하여 memoize를 해야 했다. 또한, controller를 위해 ref도 사용했다.

반드시 effect내에서 사용할 함수는 외부가 아닌 내부에서 정의 해야 한다.

관련 글

  • #ai#essay#frontend

    프론트엔드는 어디서 왔고, 에이전트 이후 어디로 가는가

    층은 왜 쌓였고, 왜 서버로 돌아왔고, 에이전트 이후에도 스택이 남는 이유는 무엇인가. 그리고 스택이 이기는 것과 그 스택을 아는 사람의 가치는 왜 별개인가

    2026-07-22·34분
  • #react#react-server-components#memoization

    React cache() 딥다이브: 소스 코드로 읽는 요청 단위 메모이제이션

    React cache() 함수의 모든 이상한 규칙은 30여 줄짜리 구현에서 직접 따라 나온다. dispatcher, getCacheForType, WeakMap/Map 트리를 소스 레벨로 따라가며 요청 단위 메모이제이션의 동작을 끝까지 본다.

    2026-05-30·35분
  • ◆ 디렉티브 딥다이브
    #react#nextjs#frontend

    'use cache' 디렉티브 딥다이브: 캐시 경계의 끝까지

    "use cache" 한 줄이 만드는 빌드 타임 변환, 캐시 키 직렬화, ResumeDataCache, cacheHandler, 그리고 Cache Components까지

    2026-05-01·65분
  • ◆ 디렉티브 딥다이브
    #react#nextjs#frontend

    'use client' 디렉티브 딥다이브: 클라이언트 경계의 끝까지

    "use client" 한 줄이 만드는 모듈 경계, 빌드 타임 변환, Flight 직렬화, 그리고 성능까지

    2026-05-01·52분

새 글을 놓치고 싶지 않으시다면 RSS로 구독해 주세요.

RSS 구독 →

yceffort — 프론트엔드 엔지니어입니다. 발표·기술 자문·기고 문의는 이곳에서 받고 있습니다.

← Back to the blogIssue on GitHub →