본문으로 건너뛰기
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
2021-02-25 · 3분
3min
2021year
KOoriginal
reactjavascript

아래 코드는 일반적으로 useEffect를 활용해서 데이터를 가져오는 방식이다.

import React, {useEffect} from 'react'

export default function App() {
  const [todo, setTodo] = useState(null)
  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch(
        'https://jsonplaceholder.typicode.com/todos/1',
      )
      const newData = await response.json()
      setTodo(newData)
    }
    fetchData()
  }, [])
  if (data) {
    return <div>{data.title}</div>
  } else {
    return null
  }
}

dependency에 아무것도 넣지 않음으로써, 딱 한번만 실행되게 끔하고 싶었지만, 이는 여전히 레이스 컨디션과 메모리 누수에 취약하다. 만약 서버에서 응답이 오는 시간이 길어졌고, 그 사이에 컴포넌트가 unmount 되었다고 생각해보자. 컴포넌트는 사라졌지만, 여전히 요청은 대기 중이다. 그리고 요청이 온다면, setTodo에 값을 넣을 것이며, 리액트는 이제 아래와 같은 경고문을 내뱉을 것이다.

Can’t perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

마찬가지로, id를 의존성 목록에 넣어서 처리하는 경우도 있을 수 있다.

import React, {useEffect} from 'react'
export default function App({id}) {
  const [todo, setTodo] = useState(null)
  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch(
        `https://jsonplaceholder.typicode.com/todos/${id}`,
      )
      const newData = await response.json()
      setTodo(newData)
    }
    fetchData()
  }, [id])
  if (data) {
    return <div>{data.title}</div>
  } else {
    return null
  }
}

이 경우에도 마찬가지로, ID가 변경되었지만 요청은 여전히 오지 않는 경우 위와 같은 문제가 있을 수 있다.

해결책

useEffect(() => {
  let isComponentMounted = true
  const fetchData = async () => {
    const response = await fetch('https://jsonplaceholder.typicode.com/todos/1')
    const newData = await response.json()
    if (isComponentMounted) {
      setTodo(newData)
    }
  }
  fetchData()
  return () => {
    isComponentMounted = false
  }
}, [])

unmount가 될 시에 요청이 늦게 와도 setTodo를 방지함으로써 문제를 해결할 수 있다. 그러나 물론 백그라운드에서는 여러개의 요청이 날라가고 있기 때문에 레이스 컨디션 문제가 발생할 수는 있다. 그래도 어쨌든, 마지막 요청의 결과만 UI에 표시된다.

더욱 확실한 방법은, http fetch를 취소하는 AbortController를 사용하는 것이다.

useEffect(() => {
  let abortController = new AbortController()
  const fetchData = async () => {
    try {
      const response = await fetch(
        'https://jsonplaceholder.typicode.com/todos/1',
        {
          signal: abortController.signal,
        },
      )
      const newData = await response.json()
      setTodo(newData)
    } catch (error) {
      if (error.name === 'AbortError') {
        // requset를 abort하는 과정에서 에러 발생
      }
    }
  }
  fetchData()
  return () => {
    abortController.abort()
  }
}, [])

unmount가 되면 cleanup을 통해서 요청을 중단시켰다. 물론, AbortController를 사용하기 위해서는 polyfill도 필요할 것이다.

관련 글

  • #react#javascript#web-performance

    React의 "서버로 회귀"는 역사의 반복인가?

    RSC가 PHP/JSP 시절로의 회귀인지, 아니면 나선형 발전일까? 아닐까? 뭘까

    2026-01-04·37분
  • #react#javascript#testing

    React의 새로운 lint 규칙: set-state-in-effect

    Effect에서 setState를 호출하면 안 되는 이유와 대안

    2025-12-16·12분
  • #react#javascript

    리액트의 신규 훅, "use"

    상황에 따라 이름이 변경되거나 사라질 수도 있습니다

    2023-06-13·25분
  • #react#javascript

    리액트 v18 버전 톺아보기

    큰거 왔다

    2022-04-04·24분

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

RSS 구독 →

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

← Back to the blogIssue on GitHub →