JavaScript

JavaScript 클로저와 React Hooks의 동작 원리 이해하기

Chrysans 2025. 2. 20. 14:12
728x90
반응형

javascript closure

JavaScript 클로저와 React Hooks의 동작 원리 이해하기

프론트엔드 개발을 하다 보면 클로저(Closure)라는 개념을 자주 마주치게 됩니다. 특히 React Hooks를 사용할 때 클로저는 핵심적인 역할을 합니다. 하지만 많은 개발자들이 클로저의 개념을 어렵게 느끼고, React Hooks와의 관계를 명확히 이해하지 못하고 있습니다.

이 글에서는 클로저의 기본 개념부터 시작해서, React Hooks가 어떻게 클로저를 활용하는지 상세히 알아보겠습니다.

목차

  1. 클로저란 무엇인가?
  2. 클로저의 동작 원리
  3. 클로저가 중요한 이유
  4. React에서의 클로저
  5. React Hooks와 클로저의 관계
  6. 실제 사용 사례와 주의점

클로저란 무엇인가?

클로저는 함수가 자신이 생성될 당시의 환경을 기억하는 현상을 말합니다. 조금 더 전문적으로 말하면, 함수와 그 함수가 선언된 렉시컬 환경(Lexical Environment)의 조합입니다.

 

쉽게 이해하기 위해 일상생활의 예시를 들어보겠습니다:

여러분이 가방을 들고 있다고 상상해보세요. 이 가방 안에는 여러분의 물건들이 있습니다. 가방을 들고 다른 장소로 이동해도, 가방 안의 물건들은 그대로 유지됩니다. 이것이 바로 클로저와 비슷합니다. 함수가 '가방'이고, 함수가 접근할 수 있는 변수들이 '가방 안의 물건들'인 셈입니다.


간단한 예제로 살펴보겠습니다:

function createCounter() {
  let count = 0;  // 프라이빗 변수

  return function() {
    return ++count;
  };
}

const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2

이 예제에서 createCounter가 반환하는 함수는 자신이 생성될 때의 환경(count 변수가 있는 환경)을 기억합니다. 이것이 바로 클로저입니다.

클로저의 동작 원리

클로저가 동작하는 방식을 좀 더 자세히 살펴보겠습니다.

1. 렉시컬 스코프 (Lexical Scope)

JavaScript는 렉시컬 스코프을 사용합니다. 이는 함수가 자신이 작성된 위치의 스코프를 기억한다는 의미입니다.

function outer() {
  const message = 'Hello';  // 외부 함수의 변수

  function inner() {
    console.log(message);  // 외부 함수의 변수에 접근
  }

  return inner;
}

const sayHello = outer();
sayHello();  // "Hello" 출력

여기서 중요한 점은:

  • inner 함수는 자신이 생성된 환경(outer 함수의 스코프)을 기억합니다.
  • outer 함수가 실행을 마쳐도, inner 함수는 여전히 message 변수에 접근할 수 있습니다.

2. 스코프 체인 (Scope Chain)

클로저는 스코프 체인을 통해 동작합니다:

  1. 함수는 자신의 지역 스코프를 먼저 살펴봅니다.
  2. 찾는 변수가 없으면 외부 스코프를 찾아봅니다.
  3. 이 과정을 전역 스코프까지 반복합니다.

3. 가비지 컬렉션 (Garbage Collection)

일반적으로 함수 실행이 끝나면 해당 함수의 지역 변수들은 가비지 컬렉션의 대상이 됩니다. 하지만 클로저가 형성되면:

  • 클로저가 참조하는 변수들은 메모리에서 해제되지 않습니다.
  • 이 변수들은 클로저가 존재하는 한 계속 유지됩니다.

클로저가 중요한 이유

클로저는 다음과 같은 중요한 기능을 제공합니다:

1. 데이터 프라이버시

클로저를 통해 private 변수와 비슷한 효과를 얻을 수 있습니다:

function createUser(name) {
  // private 변수
  let privateName = name;

  return {
    getName() {
      return privateName;
    },
    setName(newName) {
      privateName = newName;
    }
  };
}

2. 상태 유지

클로저는 함수가 자신의 상태를 유지할 수 있게 해줍니다:

function createLogger(prefix) {
  let count = 0;  // 로그 카운트 유지

  return function(message) {
    count++;
    console.log(`${prefix} (${count}): ${message}`);
  };
}

3. 모듈 패턴 구현

클로저를 통해 모듈화된 코드를 작성할 수 있습니다:

const userModule = (function() {
  let users = [];  // private 데이터

  return {
    addUser(user) {
      users.push(user);
    },
    getUsers() {
      return [...users];  // 복사본 반환
    }
  };
})();

React에서의 클로저

React에서 클로저는 특히 중요한 역할을 합니다. React의 핵심 기능들이 클로저를 기반으로 구현되어 있기 때문입니다.

1. 컴포넌트의 상태 관리

React 컴포넌트는 클로저를 통해 자신의 props와 state를 기억합니다:

function Counter() {
  const [count, setCount] = React.useState(0);

  // 이벤트 핸들러는 클로저를 형성하여 count에 접근
  const handleClick = () => {
    setCount(count + 1);
  };

  return <button onClick={handleClick}>{count}</button>;
}

 

 

React Hooks와 클로저의 관계

React Hooks는 클로저의 원리를 활용하여 구현되어 있습니다. 각각의 Hook이 어떻게 클로저를 활용하는지 살펴보겠습니다.

1. useState와 클로저

useState Hook은 클로저를 통해 컴포넌트의 상태를 관리합니다:

  • React는 내부적으로 각 컴포넌트의 상태를 배열로 관리합니다.
  • 클로저를 통해 이 배열에 접근하고 업데이트합니다.
  • 각 렌더링마다 새로운 클로저가 생성되지만, 상태 값은 React에 의해 보존됩니다.

2. useEffect와 클로저

useEffect는 클로저를 통해 의존성 배열의 값들을 기억합니다:

function SearchComponent({ query }) {
  useEffect(() => {
    // 이 함수는 클로저를 형성하여 query 값을 기억
    const search = async () => {
      const results = await searchAPI(query);
      // ...
    };

    search();
  }, [query]); // query가 변경될 때마다 새로운 클로저 생성
}

여기서 주의할 점:

  • 이펙트 함수는 렌더링 시점의 props와 state를 캡처합니다.
  • 의존성 배열이 변경되면 새로운 클로저가 생성됩니다.
  • cleanup 함수도 자신만의 클로저를 형성합니다.

3. useCallback과 클로저

useCallback은 클로저를 활용하여 함수를 메모이제이션합니다:

  • 메모이제이션된 함수는 자신이 생성된 시점의 props와 state를 기억합니다.
  • 의존성 배열이 변경되면 새로운 클로저가 생성됩니다.

실제 사용 사례와 주의점

1. Stale Closure 문제

가장 흔한 실수 중 하나는 'stale closure' 문제입니다:

function Counter() {
  const [count, setCount] = React.useState(0);

  // ❌ 문제가 있는 코드
  React.useEffect(() => {
    const timer = setInterval(() => {
      setCount(count + 1); // 오래된 count 값을 참조
    }, 1000);

    return () => clearInterval(timer);
  }, []);

  // ✅ 올바른 해결책
  React.useEffect(() => {
    const timer = setInterval(() => {
      setCount(prev => prev + 1); // 함수형 업데이트 사용
    }, 1000);

    return () => clearInterval(timer);
  }, []);
}

2. 성능 최적화

클로저를 활용한 성능 최적화:

function SearchInput({ onSearch }) {
  // 디바운스된 검색 함수
  const debouncedSearch = React.useCallback(
    debounce((term) => {
      onSearch(term);
    }, 500),
    [onSearch]
  );

  return (
    <input
      onChange={e => debouncedSearch(e.target.value)}
    />
  );
}

마무리

클로저는 JavaScript와 React의 핵심 개념입니다. 특히 React Hooks는 클로저의 원리를 기반으로 동작하며, 이를 이해하는 것은 효과적인 React 애플리케이션 개발에 매우 중요합니다.

클로저를 잘 활용하면:

  • 더 깔끔하고 모듈화된 코드를 작성할 수 있습니다.
  • 상태 관리를 효과적으로 할 수 있습니다.
  • React Hooks의 동작 원리를 더 잘 이해하고 활용할 수 있습니다.

하지만 주의할 점도 있습니다:

  • 불필요한 클로저는 메모리 누수를 일으킬 수 있습니다.
  • Stale closure 문제에 주의해야 합니다.
  • 의존성 배열 관리에 신경 써야 합니다.

이러한 개념들을 잘 이해하고 적절히 활용한다면, 더 효과적이고 버그 없는 React 애플리케이션을 개발할 수 있을 것입니다.

참고 자료

 

 

https://covelope.tistory.com/entry/guide-to-react-javascript-memory-leak-analysis-and-optimization

 

React & JavaScript 메모리 누수 분석과 해결 방법 - 최적화 노하우

안녕하세요! 오늘은 React와 JavaScript 애플리케이션에서 발생하는 메모리 누수 문제를 깊이 있게 다뤄보겠습니다. 실제 프로젝트에서 마주칠 수 있는 다양한 시나리오와 해결 방법을 함께 알아보

covelope.tistory.com

https://covelope.tistory.com/entry/Javascript-core-concepts-prototype-execution-context-this

 

JavaScript 핵심 개념 이해하기 : 프로토타입, 실행 컨텍스트, this 바인딩 정리

JavaScript를 배우면서 가장 어려운 개념을 꼽으라면 단연 프로토타입, this, 그리고 실행 컨텍스트라고 말할 수 있을 거 같습니다. 우연히 좋은 내용에 글을 보고 이해한 내용을 정리하도록 하겠습

covelope.tistory.com

 

https://covelope.tistory.com/entry/React-useState-implementation

 

리액트(React) - useState 내부 구현과 동작 원리(feat : 클로저)

리액트에서 함수형 컴포넌트 이전 클래스형 컴포넌트 사용시에는 상태를 지역변수 state에 정의하고 상태를 변경할 메소드 안에 setState 메소드를 넣어서 상태를 변경 했다.그럼 왜 state변수를 직

covelope.tistory.com

 

 

 

 

 

728x90
반응형