안녕하세요 :)
오늘은 React Hooks 중 하나인 useEffect의 클린업 함수의 필요성에 대해서 알아보도록 하겠습니다.
만약, useEffect를 모르신다면 이전 글을 읽고 오시면 이해하는데 도움이 될 거라고 생각합니다. -> useEffect란?
React의 useEffect에서 클린업 함수는 컴포넌트가 언마운트되거나, 의존성 배열에 명시된 값들이 변경되기 전에 실행되어 사이드 이펙트를 정리하는 데 중요한 역할을 합니다. 이번 글에서는 클린업 함수의 다양한 활용 사례부터 클린업 함수가 없을 경우 발생할 수 있는 문제점과 클린업 함수가 필요한 이유에 대해서 알아보겠습니다.
Cleanup Function에 다양한 활용 사례
1. 이벤트 리스너 제거
import React, { useEffect } from 'react';
export default function ExampleRemoveEventListener(){
useEffect(() => {
const handleKeyPress = (event) => {
console.log('Key Press: ${event.key}');
}
window.addEventListener('keydown', handleKeyPress);
console.log('keydown 이벤트 리스너가 추가되었습니다.');
return() => {
window.removeEventListener('keydown', handleKeyPress);
console.log('keydown 이벤트 리스너가 제거되었습니다');
};
},[]); // 비어있는 의존성 배열
return(
<div>
키보드를 눌러서 개발자 모드의 콘솔을 확인해주세요.
</div>
)
};
- 실행 결과 및 설명
- 컴포넌트가 마운트 될 때
- handleKeyPress 함수가 선언되고, keydown 이벤트에 대한 리스너가 추가됩니다.
- 'keydown 이벤트 리스너가 추가되었습니다.'라는 로그가 콘솔에 출력됩니다.
- 이벤트 발생 시
- 사용자가 키보드를 누르면, handleKeyPress 함수가 호출되어 'Key Press: [눌린 키]'가 콘솔에 출력됩니다.
- 컴포넌트가 언마운트 될 때
- 클린업 함수가 실행되어 keydown 이벤트 리스너가 제거됩니다.
- 'keydown 이벤트 리스너가 제거되었습니다.'라는 로그가 콘솔에 출력됩니다.
- 컴포넌트가 마운트 될 때
2. 타이머 정리
import React, { useState, useEffect } from 'react';
export default function ExampleTimer(){
const [time, setTime] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setTime(prev => prev + 1);
},1000);
console.log('타이머가 시작되었습니다.');
return() => {
clearInterval(interval);
console.log('타이머가 종료되었습니다.');
}
}, []); // 비어있는 의존성 배열
return(
<div>
타이머: {time}초
</div>
);
};
- 실행 결과 및 설명
- 컴포넌트가 마운트될 때
- useState를 사용하여 time이라는 상태 변수를 선언하고 초기값을 0으로 설정합니다.
- 이 상태는 화면에 타이머로 표시됩니다.
- 컴포넌트가 마운트될 때 useEffect가 실행됩니다.
- 내부에서 setInterval을 사용하여 1초(1000ms)마다 time 상태를 1씩 증가시키는 타이머를 설정합니다.
(setInterval: 일정 시간 간격을 두고 반복적으로 실행할 때 사용하는 함수) - 콘솔에 '타이머가 시작되었습니다.'라는 로그가 출력됩니다.
- 타이머 동작 중
- 설정된 setInterval 함수는 매 1초마다 실행되어 time 상태를 1씩 증가시킵니다.
- setTime(prev => prev + 1)는 이전 상태 값을 기반으로 안전하게 상태를 업데이트합니다.
- time 상태가 업데이트될 때마다 컴포넌트가 재렌더링되어 화면에 타이머가 실시간으로 표시됩니다.
- 예를 들어, time이 1이 되면 화면에는 '타이머: 1초'가 표시됩니다.
- 컴포넌트가 언마운트될 때
- 컴포넌트가 언마운트될 때 useEffect의 클린업 함수가 실행됩니다.
- clearInterval(interval)을 호출하여 설정된 타이머를 중단시킵니다.
- 콘솔에 '타이머가 종료되었습니다.'라는 로그가 출력됩니다.
- 컴포넌트가 마운트될 때
3. 웹소켓 구독
import React, { useState, useEffect } from 'react';
export default function ExampleSubScribe() {
const [message, setMessage] = useState([]);
useEffect(() => {
const socket = new WebSocket('wss://example.com/socket');
socket.onmessage = (event) => {
setMessage = (prev => [...prev, event.data]);
}
console.log('웹소켓이 연결되었습니다.');
return() => {
socket.close()
console.log('웹소켓 연결이 끊어졌습니다.');
};
},[]);
return(
<div>
<h1> 메시지 목록 </h1>
<ul>
{message.map((msg, index) =>
<li key={index}>
{msg}
</li>
)};
</ul>
</div>
);
};
- 실행 결과 및 설명
- 컴포넌트가 마운트될 때
- useEffect 내부에서 new WebSocket('wss://example.com/socket')을 통해 웹소켓 연결이 자동으로 시작됩니다.
- '웹소켓이 연결되었습니다.'라는 메시지가 콘솔에 출력됩니다.
- 웹소켓을 통해 서버로부터 메시지가 수신될 때마다 socket.onmessage 핸들러가 호출되어, 수신한 메시지를 messages 배열에 추가합니다
- 이로 인해, messages 상태는 실시간으로 업데이트되며, 화면에 메시지 목록이 동적으로 표시됩니다.
- 메시지 수신 시
- 서버로부터 메시지가 도착하면, socket.onmessage 핸들러가 호출됩니다.
- setMessages 함수를 통해 기존 messages 배열에 새로운 메시지가 추가됩니다.
- 화면에는 messages.map()을 활용하여 수신된 모든 메시지가 <li> 요소로 표시됩니다.
- 컴포넌트가 언마운트될 때
- 클린업 함수인 socket.close();가 실행되어 웹소켓 연결을 종료합니다.
- '웹소켓 연결이 끊어졌습니다.'라는 메시지가 콘솔에 출력됩니다.
- 컴포넌트가 마운트될 때
클린업 함수가 없다면?
클린업 함수가 없다면 컴포넌트의 생명주기 동안 발생하는 사이드 이펙트를 적절히 정리하지 못하게 되어 여러 문제가 발생할 수 있습니다.
1. 메모리 낭비
- 클린업 함수를 사용하지 않으면, 리벤트 리스너나 타이머 같은 리소스가 컴포넌트가 언마운트된 후에도 계속해서 메모리에 남아있게 됩니다.
2. 불필요한 상태 업데이트
- 컴포넌트가 언마운트된 후에도 비동기 작업(데이터 페칭, 타이머 등)이 완료되면, 여전히 상태를 업데이트하려 시도할 수 있습니다. 이렇게 되면 React에서는 경고 메시지를 발생시키거나, 예기치 않은 동작 유발할 수 있다.
3. 성능 저하
- 이벤트 리스너나 타이머 같은 함수가 계속 실행되어 애플리케이션의 성능을 저하시킬 수 있습니다.
- 계속되서 렌더링 되는 컴포넌트의 경우 이 문제는 더 심각하게 다가갈 수 있습니다.
이러한 문제들이 어떻게 나타나게 되는 걸까요?
예시로 '클린업 함수가 없는 비동기 데이터 페칭' 코드를 통해 위와 같은 문제점이 어떻게 나타나게 되는 것인지 알아보도록 하겠습니다.
import React, { useState, useEffect } from 'react';
export default function IfNotCleanUpFuncDataFetching() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('https://example.com');
.then(reponse => reponse.json)
.then(result => {
setData(result); // 컴포넌트가 언마운트된 후에도 호출될 수 있음.
})
.catch(error =>
console.log(error)
);
},[]);
return(
<div>
데이터를 로딩 중입니다...!!
</div>
)
};
- 컴포넌트가 마운트 될 때
- 컴포넌트가 마운트 되면서 useEffect가 실행됩니다.
- fetch() 함수를 통해 데이터 요청이 시작됩니다.
(이 때 보내는 요청은 네트워크 상태나 서버 응답 속도에 따라 일정 시간이 소요될 수 있습니다.)
- 비동기 작업 진행 중
- 사용자가 데이터 로딩을 완료하기 전에 페이지 이동, 컴포넌트 숨기기 등을 통해 해당 컴포넌트를 언마운트 시킬 수 있습니다.
- 컴포넌트가 언마운트 되면, 해당 컴포넌트는 더 이상 DOM에 존재하지 않지만 fetch()함수을 통한 데이터 요청은 여전히 계속되고 있습니다.
- 불필요하게 데이터 요청이 이루어지면 이 과정에서 메모리 누수 문제가 발생합니다.
- 비동기 작업 완료
- 비동기 작업이 완료되면 .then() 블록이 실행됩니다.
- setData(result)가 실행되면서 상태를 업데이트를 시도하게 됩니다.
- 하지만 이 시점에서는 이미 컴포넌트가 언마운트 되었기 때문에 상태 업데이트가 불가합니다.
- 경고 메시지 발생
- React는 언마운트된 컴포넌트에 대한 상태 업데이트가 수행되려 할 때 경고 메시지를 보냅니다.
Warning: 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.
이처럼 클린업 함수를 사용하지 않고 누적해서 useEffect를 사용하게 되면, 성능 저하로 문제가 심각하게 이어질 수 있습니다.
끝내는 말
이번 글에서는 'useEffect에서 클린업 함수를 왜 쓰는가?'에 대해 알아보았습니다. 클린업 함수는 useEffect에서 일어날 수 있는 에러사항을 방지할 수 있는 중요한 요소입니다. 자칫하면 애플리케이션의 성능 저하로 이어질 수 있으니 유의해서 사용하셨으면 좋겠습니다!
읽어주셔서 감사합니다:)
'JavaScript > React' 카테고리의 다른 글
React 컴포넌트란 무엇인가. (2) | 2024.12.29 |
---|---|
React에서 Debounce 사용하기 (1) | 2024.11.08 |
Context API로 전역 상태 관리하기 (2) | 2024.10.28 |
useRef란? (3) | 2024.10.23 |
useEffect란? (1) | 2024.10.08 |