2024. 12. 7. 18:31ㆍReact
함수형 컴포넌트의 구조와 동작 방식(feat. 클로저)
React의 함수형 컴포넌트는 클로저 개념과 깊은 관련이 있습니다. 이 글에서는 클로저가 무엇인지 알아보고, 함수형 컴포넌트에서 클로저가 어떻게 활용되는지 살펴보겠습니다.
클로저
- 클로저는 함수와 함수가 선언된 어휘적 환경(Lexical Scope)의 조합이다.
- "선언된 어휘적 환경"이라는 것은, 변수가 코드 내부 어디에서 선언됐는지를 말하는 것
- 호출되는 방식에 따라 동적으로 결정되는 this와 다르게, 코드가 작성된 순간에 정적으로 결정된다.
- 함수가 정의된 시점의 렉시컬 스코프(변수 환경)를 기억하고, 그 범위 안의 변수들에 접근할 수 있는 함수를 의미
function add() {
const a = 10; //a 변수의 유효 범위는 add 전체
function innerAdd() {
const b = 20; //b 변수의 유효 범위는 innerAdd 전체
console.log(a + b); //innerAdd는 add 내부에 선언돼 있어 변수 a를 접근할 수 있게 됨
}
innerAdd();
}
add();
전역 스코프(global scope)
- 스코프는 변수의 유효 범위이고, 전역 레벨에 선언하는 것이 전역 스코프
- 전역 스코프에 변수를 선언하면 어디서든 호출할 수 있다.
- 전역 객체에 전역 레벨에서 선언한 스코프가 바인딩된다.
- 브라우저 환경에서 전역 객체는 window, Node.js 환경에서는 global이다.
var a = 10; // 전역 변수 선언 (전역 객체에 바인딩)
function greet() {
console.log("Hello!"); // 전역 함수 선언 (전역 객체에 바인딩)
}
console.log(window.a); // 10 (브라우저 환경)
console.log(window.greet); // function greet() { ... }
함수 스코프
💡 자바스크립트는 기본적으로 함수 레벨 스코프를 따른다.
💡 {} 블록이 스코프 범위를 결정하지 않는다.
- var로 선언된 변수는 함수 레벨 스코프를 따른다.
function test() {
if (true) {
var x = 10;
}
console.log(x); // 10, if 블록 외부에서도 접근 가능
}
test();
- let이나 const로 선언된 변수는 블록 레벨 스코프를 따르므로, if 블록 내에서만 유효하다.
function test() {
if (true) {
let y = 20;
}
console.log(y); // ReferenceError: y is not defined, if 블록 외부에서 접근 불가
}
test();
주의할 점
아래 코드는 의도와 다르게 5만 다섯 번 출력한다.
for(var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, i * 1000)
}
- i가 전역 변수로 작동하기 때문이다.
- for 문을 다 순회한 이후, 태스크 큐에 있는 setTimeout을 실행하려고 했을 때, 이미 전역 레벨에 있는 i는 5로 업데이트가 되어 있다.
올바르게 수정하는 방법으로는 첫째, 블록 레벨 스코프를 갖는 let으로 수정하는 것
for(let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i); //의도대로 0, 1, 2, 3, 4 출력
}, i * 1000)
}
두 번째로는 클로저를 활용하는 것
for(var i = 0; i < 5; i++) {
setTimeout(
(function(sec) { //즉시 실행 익명 함수
return function() {
console.log(sec);
}
})(i),
i * 1000,
)
}
- for문을 순회할 때마다 즉시 실행 익명 함수가 생성되고 실행되기를 반복한다.
- 각각의 함수는 고유한 스코프, 고유한 sec값을 i로부터 받아 올바르게 실행할 수 있게 된다.
- 클로저는 생성될 때마다 그 선언적 환경을 기억해야 하므로 추가적인 비용이 발생한다.
- 클로저에 꼭 필요한 작업만 남겨두지 않는다면 메모리를 불필요하게 잡아먹는 결과를 야기할 수 있고, 마찬가지로 클로저 사용을 적절한 스코프로 가둬두지 않는다면 성능에 악영향을 미친다.
리액트에서의 클로저
클로저의 원리를 사용하고 있는 대표적인 것 중 하나가 useState이다.
- useState는 함수형 컴포넌트 내에서 상태를 정의하고, 이 상태를 관리할 수 있게 해주는 훅이다.
- 함수(useState) 내부에 선언된 함수(setState)가 함수의 실행이 종료된 이후에도(useState가 호출된 이후에도) 지역변수인 state를 계속 참조할 수 있는 이유가 바로 클로저이다.
- 매번 실행되는 함수형 컴포넌트 환경에서 state 값을 유지하고 사용하기 위해서 리액트는 클로저를 활용한다.
function Component() {
const [state, setState] = useState();
function handleClick() {
//useState 호출은 위에서 끝났지만, setState는 계속 내부의 최신값(prev)을 알고 있다.
//이는 클로저를 활용했기 때문에 가능하다.
setState((prev) => prev + 1);
}
//...
}
💡 게으른 초기화(Lazy initialization)
- useState에 변수 대신 함수를 넘기는 것
- 게으른 초기화 함수는 오로지 state가 처음 만들어질 때만 사용된다. 이후 리렌더링 발생 시 실행되지 않는다.
- localStorage나 sessionStorage에 대한 접근, map, filter, find 같은 배열에 대한 접근 혹은 초깃값 계산을 위해 함수 호출이 필요할 때와 같이 무거운 연산을 포함해 실행 비용이 많이 드는 경우에 사용하는 것이 좋다.
// 일반적인 useState 사용 // 바로 값을 전달 const [count, setCount] = useState( Number.parseInt(window.localStorage.getItem(cacheKey)); } // 게으른 초기화 // 함수를 실행해 값을 반환한다는 점이 다르다. const [count, setCount] = useState(() => Number.parseInt(window.localStorage.getItem(cacheKey)), )
참고
모던 리액트 Deep Dive
MDN - 클로저
'React' 카테고리의 다른 글
왜 리액트인가? (2) | 2024.11.20 |
---|---|
Canvas로 그림 그리기 기능 구현: 이슈와 해결 과정 (0) | 2024.05.26 |