- Published on
useEffect와 메모리 누수
- Author
- Name
- yceffort
아래 코드는 일반적으로 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도 필요할 것이다.