클로저와 리액트: 메모리 관리를 위한 기본 원칙

jsonLee·2023년 8월 18일
0
post-thumbnail

오늘은 JavaScript의 중요한 개념 중 하나인 '클로저(Closure)'에 대해 알아보겠습니다. 리액트를 사용하면서도 클로저와 깊은 관계가 있기 때문에 이해하고 넘어가면 도움이 될 것입니다.

1. 클로저란?

클로저는 함수와 그 함수가 생성될 때의 환경정보 사이의 조합입니다. 다른 말로, 내부 함수가 외부 함수의 변수에 접근할 수 있게 하는 것을 의미합니다.

예제:

function outerFunction() {
  let outerVariable = '바깥 변수';

  function innerFunction() {
    console.log(outerVariable);
  }

  return innerFunction;
}

const myClosure = outerFunction();
myClosure();  // 출력: '바깥 변수'

위의 예제에서 innerFunction은 outerFunction의 outerVariable에 접근합니다. outerFunction이 종료된 후에도 outerVariable에 접근할 수 있기 때문에 클로저라고 부릅니다.

2. 클로저의 활용

클로저는 여러 상황에서 활용될 수 있습니다.

  • 데이터 은닉과 캡슐화: 클로저를 사용하여 private 변수를 만들 수 있습니다.
  • 동적 함수 생성: 실행 시점에 적절한 기능을 가진 함수를 생성할 때 클로저를 활용할 수 있습니다.
  • 콜백에서 상태 유지: 비동기 콜백에서 상태를 유지할 때 클로저를 활용할 수 있습니다.

3. 리액트와 클로저

리액트에서는 상태와 이벤트 핸들러 내에서 클로저를 자주 볼 수 있습니다. 예를 들면, useState의 상태 업데이트 함수나 useEffect 내부에서 이전 상태나 prop에 접근할 때 클로저가 활용됩니다.

4. 주의사항

클로저는 강력하지만, 메모리 누수 등의 문제를 초래할 수도 있습니다. 사용 후에는 필요없는 클로저는 제거해주는 것이 좋습니다.

메모리 누수를 피하기 위한 클로저의 관리는 중요한 주제입니다. 클로저가 계속 참조하고 있으면 해당 클로저에 연결된 변수들은 가비지 컬렉션 대상에서 제외됩니다. 이는 메모리 누수를 초래할 수 있습니다.

간단한 예제로, DOM 요소에 이벤트 핸들러를 추가하면서 클로저를 사용하는 경우를 들 수 있습니다.

예제:
HTML에는 다음과 같은 버튼이 있습니다:

<button id="myButton">Click me!</button>

JavaScript에서 다음과 같이 이벤트 핸들러를 추가합니다:

function attachEventHandler() {
    const button = document.getElementById('myButton');
    let clickCount = 0;  // 이 변수는 클로저에 의해 참조됩니다.

    button.addEventListener('click', function() {
        clickCount++;
        console.log(`Button clicked ${clickCount} times.`);
    });
}

위의 코드에서, 클릭 이벤트 핸들러는 clickCount 변수를 참조하고 있으므로 클로저가 형성됩니다. 만약에 이 버튼을 DOM에서 제거한다면, 이 클로저는 여전히 clickCount를 참조하게 되어 메모리를 계속 차지하게 됩니다.

이 문제를 해결하기 위해서는 버튼을 제거하기 전에 이벤트 핸들러도 제거해주는 것이 좋습니다:

function createEventHandler() {
    let clickCount = 0;  // 클로저를 통해 접근될 변수

    return function handleClick() {
        clickCount++;
        console.log(`Button clicked ${clickCount} times.`);
    };
}

const handler = createEventHandler();

function attachEventHandler() { // 익명함수가 아닌 핸들러를 담는다
    const button = document.getElementById('myButton');
    button.addEventListener('click', handler);
}

function removeEventHandler() { // 같은 참조 핸들러를 담는다
    const button = document.getElementById('myButton');
    button.removeEventListener('click', handler);
}

이렇게 이벤트 핸들러를 제거하면, 클로저가 참조하는 변수들도 가비지 컬렉션의 대상이 될 수 있어 메모리 누수를 방지할 수 있습니다.

5. React에서의 활용

리액트에서는 컴포넌트의 라이프 사이클과 훅을 이용하여 리소스 관리와 이벤트 리스너의 추가 및 제거를 쉽게 처리하여 메모리 누수를 피할 수 있습니다.

함수 컴포넌트에서는 useEffect 훅을 이용해 이벤트 리스너의 추가 및 제거를 처리할 수 있습니다. useEffect의 반환 값으로 함수를 제공하면, 이 함수는 컴포넌트 언마운트 시 실행됩니다. 이를 "cleanup 함수"라고도 합니다.

import React, { useState, useEffect } from 'react';

function MyButtonComponent() {
  const [clickCount, setClickCount] = useState(0);

  useEffect(() => {
    function handleClick() {
      setClickCount(prevCount => prevCount + 1);
    }

    document.addEventListener('click', handleClick);

    // cleanup 함수에서 핸들러를 제거
    return () => {
      document.removeEventListener('click', handleClick);
    };
  }, []); // 의존성 배열이 비어 있으므로 마운트와 언마운트 시에만 실행됩니다.

  return <div>
    <button>Click me</button>
    <p>Button clicked {clickCount} times.</p>
  </div>;
}

export default MyButtonComponent;

이 코드에서는:

  • useState를 사용하여 클릭 횟수를 저장하고 화면에 표시합니다.
  • useEffect 내에서 클릭 이벤트 리스너를 추가하고, cleanup 함수에서 해당 리스너를 제거합니다.
  • 클릭 시마다 handleClick 함수를 통해 상태를 업데이트하고, 이로 인해 컴포넌트가 다시 렌더링됩니다.
    이런 방식으로 useState와 useEffect를 함께 사용하면 클릭 횟수를 상태로 관리하면서도 이벤트 리스너의 관리도 쉽게 할 수 있습니다.

REFERNCE

https://developer.mozilla.org/ko/docs/Web/JavaScript/Closures
https://ko.javascript.info/closure

profile
풀스택 개발자

2개의 댓글

comment-user-thumbnail
2023년 8월 18일

좋은 정보 감사합니다

1개의 답글