들어가기 전 | 리액트와 클로저의 관계?
어찌보면 당연한 것일지도 모릅니다. 그런데, 평소에 신경쓰지 않고 있다가 클로저라는 개념에 대해서 잠시 상기해보는 시간을 가지게 되었는데, 리액트의 함수 컴포넌트 구조와 클로저의 구조가 유사하다는 것이 눈에 들어왔습니다.
사실 이는 당연한 것이겠죠? 왜냐 하면, 리액트 함수 컴포넌트는 결국 하나의 함수이고, return 을 통해 값을 반환하고 있기 때문에, 그 내부적으로 저리되는 상태와 해당 상태를 업데이트 할 때 이전 상태의 값을 참조하여 변경하는 작업은 결국 내부적으로 클로저의 개념을 적용할 수 밖에 없기 때문입니다.
따라서 오늘은 리액트와 클로저, 정확히는 함수 컴포넌트와 클로저의 관계에 대해서 나름의 생각을 정리해볼 것입니다.
기초개념 | 클로저란 무엇인가?
우선, 자바스크립트에서 클로저의 개념에 대해서 간략하게 정리해볼까 합니다.
클로저는 쉽게 말하면, 현재 함수가 자신이 속한 스코프의 환경(ex. 변수 등)을 기억하고 있는 것입니다. 예를 들어, 다음과 같은 고차함수가 있습니다.
function createCounter() {
let count = 0;
return function() {
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
createCounter 함수는 내부적으로 익명함수를 반환하고 있습니다. 여기서 익명함수를 내부함수라고 지칭하겠습니다.
내부함수는 자신이 속한 스코프(createCounter 함수의 스코프)의 주위 환경 정보를 기억하고 있으며, 여기서는 count 라는 변수의 키와 값 정보를 참조하는 클로저를 생성하게 됩니다. 즉, createCounter 함수가 호출되어서 원래라면 let count 변수도 createCounter 함수가 호출되어 종료됨에 따라 메모리 상에서 제거되어야 하지만, 내부함수가 count 변수를 계속 참조하고 있기 때문에, 제거 되지 않고 메모리 상에 존재하게 됩니다.
이러한 특징을 클로저라고 부르며, 우리는 이 클로저 덕분에 createCounter 라는 함수의 외부에서 함수 내부의 변수인 let count 에 접근할 수 있는 것입니다.
리액트에서는 클로저가 어떻게 사용되고 있나?
그렇다면, 클로저는 리액트에서 어떻게 사용되고 있을까요? 리액트는 결국 자바스크립트로 이루어져 있기 때문에, 라이브러리 자체 코드를 파고들면 당연히 클로저 개념을 적극 사용하고 있을 겁니다. 다만, 여기서 바라볼 것은 우리가 흔히 추상화되어 접근하기 쉬운 함수 컴포넌트의 기준에서 클로저가 어떻게 사용되고 있는지 알아보겠습니다.
이벤트 핸들러에서의 클로저
우선 우리가 흔히 사용하는 방식인 이벤트 핸들러에서 클로저가 어떻게 적용되고 있는지 알아봅시다. 아래 코드는 useState 로 관리되는 count 상태를 onClick 이벤트로 등록된 리스너인 handleClick 함수가 호출 될 때 마다, 1씩 업데이트 하는 코드입니다.
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1); // 이 함수는 count 변수에 대한 클로저를 형성함
}
return (
<div>
<p>{count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
}
리액트에서 상태를 업데이트 할 때는 스냅샷 단위로 이루어지며, 자신이 속한 컴포넌트(함수) 전체를 리렌더링하게 됩니
다. 여기서, handleClick 함수는 클로저가 존재하지 않는다면, 함수가 리렌더링 되고 나서, count 의 현재 상태에 접근이 불가능 하지만, 이것이 가능한 이유는 동일한 스코프를 공유할 수 있도록 클로저가 형성되기 때문입니다.
즉, 클로저를 통해 handleClick 리스너가 속한 스코프에 존재하는 count 변수의 정보를 명확하게 참조할 수 있다는 의미입니다.
useEffect 내부에서의 클로저
아래 코드에서 setInterval 내의 콜백 함수는 seconds 상태의 최신 값을 클로저를 통해 참조합니다. 즉, seconds 라는 상태는 useEffect 내부의 콜백함수가 생성한 클로저에 의해 계속 참조하게 되고, 이를 기반으로 setState 함수 내에서 이전 상태를 바탕으로 새로운 상태를 안정적으로 업데이트할 수 있게 됩니다.
import React, { useState, useEffect } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const timerId = setInterval(() => {
// `seconds` 상태의 최신 값을 클로저를 통해 참조합니다.
setSeconds(prevSeconds => prevSeconds + 1);
}, 1000);
// 컴포넌트 언마운트 시 타이머를 정리합니다.
return () => clearInterval(timerId);
}, []); // 의존성 배열이 빈 배열이므로 컴포넌트 마운트 시에만 실행됩니다.
return (
<div>
<p>Time: {seconds}s</p>
</div>
);
}
export default Timer;
나가는 말
오늘은 간단하게 리액트와 클로저의 관계에 대해서 정리해보는 시간을 가져보았습니다.
클로저에 대해서 다시 나름의 생각을 기반으로 정리해보면, 클로저는 결국 함수가 선언되는 시점의 스코프 즉, 렉시컬 스코프를 기억하는 것인데, 여기서 렉시컬 스코프는 함수가 선언되는 위치에 따라서 상위 스코프가 결정된다는 개념 입니다.
즉, 외부 함수 내부에 선언되는 내부함수가 있다면, 해당 내부함수의 상위 스코프는 외부함수가 될 것이고, 해당 외부함수 내에 선언된 다양한 변수나 함수들은 서로 간에 참조할 수 있는 관계에 놓이게 되며, 이것이 클로저라는 것임을 이해할 수 있었습니다.
참고하면 좋은 자료
MDN 클로저: https://developer.mozilla.org/ko/docs/Web/JavaScript/Closures