부제 : 당신의 리액트 애플리케이션에서 메모리 누수를 막아요.
이 글은 ReactONE의 How to Cleanup Side Effects in React를 번역한 글입니다. 원본은 제목 링크를 클릭해서 확인해주세요.
리액트에서는 컴포넌트가 렌더링된 후에 필요한 작업이 있거나 사이드 이펙트를 일으킬 필요가 있을 때 useEffect를 사용합니다. 사이드 이펙트는 서버에서 데이터 가져오기, 로컬 스토리지에서 정보를 가져오거나 저장하기, 이벤트 리스너 또는 구독(어떤 이벤트가 발생할 때마다 서버로 그 결과를 보내는 것을 의미) 설정일 수 있습니다.
useEffect()를 사용하면 함수형 컴포넌트에서 컴포넌트 라이프 사이클을 관리할 수 있습니다. useEffect()는 componentDidMount , componentDidUpdate 및 componentWillUnmount를 조합해놓았다고 할 수 있습니다.
그러나 때로는 컴포넌트 생명주기와 사이드 이펙트 생명주기(시작, 진행 중, 완료)가 교차하는 지점에서 문제가 발생할 수 있습니다.
사이드 이펙트가 완료되었을 때, 이미 언마운트된 컴포넌트의 상태(state)를 업데이트하려고 하고 React 경고가 나타납니다.
이 게시물에서는 위의 경고가 나타나는 경우와 메모리 누수를 방지하기 위해 React에서 사이드 이펙트를 올바르게 정리하는 방법에 대해 설명합니다.
먼저 예제의 문제 상황을 재현해보겠습니다. 이 예는 Users 또는 hello world 텍스트에 대한 정보를 보여줍니다. 사용자 목록은 fetch 요청을 사용하여 로드됩니다.
function App() {
const [display, setDisplay] = useState("users");
return (
<div className='App'>
<button
onClick={() => {
setDisplay("users");
}}>
display users
</button>
<button
onClick={() => {
setDisplay("posts");
}}>
display hello message
</button>
<>{display === "users" ? <Users /> : <Hello />}</>
</div>
);
}
export default function Hello() {
return (
<p>
Hello, World !!
</p>
);
}
export default function Users() {
const [list, setList] = useState(null);
useEffect(() => {
(function () {
try {
fetch(`https://jsonplaceholder.typicode.com/users`)
.then((response) => response.json())
.then((json) => setList(json));
} catch (e) {
// Handle the error
}
})();
});
return (
<div>
{list === null ? (
<p>Loading users...</p>
) : (
<>
{list.map((item) => {
return <pre key={item.id}>{item.name}</pre>;
})}
</>
)}
</div>
);
};
Users의 fetch 함수 실행이 완료되기 전에 hello 메시지 버튼을 클릭하면 콘솔에 경고 메시지가 나타납니다.
이 경고의 이유는 해당 Users 컴포넌트가 이미 언마운트되었지만 사이드 이펙트가 완료되어 상태를 업데이트하려고 시도하기 때문입니다.
컴포넌트가 언마운트될 때, 사이드 이펙트를 취소하여 이 문제를 해결할 수 있습니다. 다음 섹션에서 더 살펴보겠습니다.
다행히 useEffect(callback, 종속성)를 사용하면 사이드 이펙트를 쉽게 정리할 수 있습니다. 콜백 함수가 함수를 반환하면 React는 이를 이벤트 정리로 사용합니다.
useEffect(() => {
// 여기에서 사이트 이펙트가 발생합니다.
return () => {
// 여기에 사이드 이펙트가 발생하는 함수를 정리합니다.
}
// 의존성 배열
}, [])
먼저 DOM 요청을 중단할 수 있는 컨트롤러를 만든 다음 컨트롤러를 fetch 요청과 연결합니다. 마지막으로 정리 기능은 컴포넌트가 언마운트되는 경우 요청을 중단합니다.
useEffect(() => {
//컨트롤러를 만듭니다.
let controller = new AbortController();
(async () => {
try {
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts`,
{
// 만든 컨트롤러와 fetch 함수를 연결합니다.
signal: controller.signal,
},
);
setList(await response.json());
controller = null;
} catch (e) {
// Handle the error
}
})();
//컴포넌트가 언마운트될 때, 요청을 중단합니다.
return () => controller?.abort();
});
사이드 이펙트가 prop 또는 state 값에 의존하고 있을 경우, fetch 요청 중단이 필요한 경우가 있습니다. 앞서 언급했듯이 useEffect()가 이 경우를 처리할 수 있습니다. 다음의 props의 id를 기반으로 직원의 세부사항을 fetch 요청하는 User 컴포넌트를 살펴보겠습니다.
export default function User({ id }) {
const [user, setUser] = useState(null);
useEffect(() => {
let controller = new AbortController();
(async () => {
try {
const response = await fetch(
`https://jsonplaceholder.typicode.com/users/${id}`,
{
signal: controller.signal,
},
);
setUser(await response.json());
controller = null;
} catch (e) {
// Handle the error
}
})();
// clean up function
return () => controller?.abort();
// add a dependency array
}, [id]);
return (
<div>
{user === null ? (
<p>Loading user's data ...</p>
) : (
<pre key={user.id}>{user.name}</pre>
)}
</div>
);
};
타이머 기능을 사용할 때 clearTimeout(timerId)를 사용하여 언마운트시 타이머 기능을 삭제할 수 있습니다.
예를 들어, 매초 자동으로 증가하는 카운터를 봅시다.
export default function AutoIncrementaedCounter() {
const [counterValue, setCounterValue] = useState(0);
useEffect(() => {
//3초마다 counter를 1씩 증가시킨다.
let timerId = setTimeout(() => {
setCounterValue(counterValue + 1);
timerId = null;
}, 3000);
// 컴포넌트를 언마운트할 때, 정리는 여기서
return () => clearTimeout(timerId);
});
return <p>{counterValue}</p>;
}
외부 데이터에 대한 구독을 설정시, 컴포넌트 마운트 해제될 때 정리하는 것이 중요합니다. 예를 들어 웹 소켓.
export default function Component() {
const [url] = useState("");
useEffect(() => {
const webSocket = new WebSocket(url);
// do stuff here
// clean up when component unmount
return () => webSocket.close();
},);
// ...
}
일부 이펙트는 메모리 누수를 방지하기 위해 정리가 필요할 수 있습니다. useEffect()를 사용하면 구성 요소가 렌더링된 후 다양한 종류의 사이드 이펙트를 수행한 다음 종류에 따라 제거할 수 있습니다.