본문 바로가기

JavaScript/React

useEffect: 클린업 함수의 필요성

안녕하세요 :)

오늘은 React Hooks 중 하나인 useEffect의 클린업 함수의 필요성에 대해서 알아보도록 하겠습니다.

만약, useEffect를 모르신다면 이전 글을 읽고 오시면 이해하는데 도움이 될 거라고 생각합니다. ->  useEffect란?

출처: https://blog.devgenius.io/how-to-use-react-clean-up-function-with-example-7a073392e479

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>
    )
};
  • 실행 결과 및 설명
    1. 컴포넌트가 마운트 될 때
      • handleKeyPress 함수가 선언되고, keydown 이벤트에 대한 리스너가 추가됩니다.
      • 'keydown 이벤트 리스너가 추가되었습니다.'라는 로그가 콘솔에 출력됩니다.
    2. 이벤트 발생 시
      • 사용자가 키보드를 누르면, handleKeyPress 함수가 호출되어 'Key Press: [눌린 키]'가 콘솔에 출력됩니다.
    3. 컴포넌트가 언마운트 될 때
      • 클린업 함수가 실행되어 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>
    );
};
  • 실행 결과 및 설명
    1. 컴포넌트가 마운트될 때
      • useState를 사용하여 time이라는 상태 변수를 선언하고 초기값을 0으로 설정합니다.
      • 이 상태는 화면에 타이머로 표시됩니다.
      • 컴포넌트가 마운트될 때 useEffect가 실행됩니다.
      • 내부에서 setInterval을 사용하여 1초(1000ms)마다 time 상태를 1씩 증가시키는 타이머를 설정합니다.
        (setInterval: 일정 시간 간격을 두고 반복적으로 실행할 때 사용하는 함수)
      • 콘솔에 '타이머가 시작되었습니다.'라는 로그가 출력됩니다.
    2. 타이머 동작 중
      • 설정된 setInterval 함수는 매 1초마다 실행되어 time 상태를 1씩 증가시킵니다.
      • setTime(prev => prev + 1)는 이전 상태 값을 기반으로 안전하게 상태를 업데이트합니다.
      • time 상태가 업데이트될 때마다 컴포넌트가 재렌더링되어 화면에 타이머가 실시간으로 표시됩니다.
      • 예를 들어, time이 1이 되면 화면에는 '타이머: 1초'가 표시됩니다.
      •  
    3. 컴포넌트가 언마운트될 때
      • 컴포넌트가 언마운트될 때 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>
    
    );
};
  • 실행 결과 및 설명
    1. 컴포넌트가 마운트될 때
      • useEffect 내부에서 new WebSocket('wss://example.com/socket')을 통해 웹소켓 연결이 자동으로 시작됩니다.
      • '웹소켓이 연결되었습니다.'라는 메시지가 콘솔에 출력됩니다.
      • 웹소켓을 통해 서버로부터 메시지가 수신될 때마다 socket.onmessage 핸들러가 호출되어, 수신한 메시지를 messages 배열에 추가합니다
      • 이로 인해, messages 상태는 실시간으로 업데이트되며, 화면에 메시지 목록이 동적으로 표시됩니다.
    2. 메시지 수신 시
      • 서버로부터 메시지가 도착하면, socket.onmessage 핸들러가 호출됩니다.
      • setMessages 함수를 통해 기존 messages 배열에 새로운 메시지가 추가됩니다.
      • 화면에는 messages.map()을 활용하여 수신된 모든 메시지가 <li> 요소로 표시됩니다.
    3. 컴포넌트가 언마운트될 때
      • 클린업 함수인 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>
    )
};
  1. 컴포넌트가 마운트 될 때
    • 컴포넌트가 마운트 되면서 useEffect가 실행됩니다.
    • fetch() 함수를 통해 데이터 요청이 시작됩니다.
      (이 때 보내는 요청은 네트워크 상태나 서버 응답 속도에 따라 일정 시간이 소요될 수 있습니다.)
  2. 비동기 작업 진행 중
    • 사용자가 데이터 로딩을 완료하기 전에 페이지 이동, 컴포넌트 숨기기 등을 통해 해당 컴포넌트를 언마운트 시킬 수 있습니다.
    • 컴포넌트가 언마운트 되면, 해당 컴포넌트는 더 이상 DOM에 존재하지 않지만 fetch()함수을 통한 데이터 요청은 여전히 계속되고 있습니다.
    • 불필요하게 데이터 요청이 이루어지면 이 과정에서 메모리 누수 문제가 발생합니다.
  3. 비동기 작업 완료
    • 비동기 작업이 완료되면 .then() 블록이 실행됩니다.
    • setData(result)가 실행되면서 상태를 업데이트를 시도하게 됩니다.
    • 하지만 이 시점에서는 이미 컴포넌트가 언마운트 되었기 때문에 상태 업데이트가 불가합니다.
  4. 경고 메시지 발생
    • 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