클로저는?
정의) A closure is the combination of a function bundled together (enclosed) with references to its surrouding state (the lexical environment).
함수가 선언될 당시의 Lexical Environment의 상호 관계에 따른 현상. 비공개 변수를 가질 수 있는 환경에 있는 함수가 클로저이다. 외부 변수를 기억하고 이 외부 변수에 접근할 수 있는 함수이다.
클로저는 열린 변수식을 닫힌 변수식으로 변환해준다. 외부 스코프의 변수에 접근해서 그 변수가 가비지 컬렉션의 대상에서 제외 되도록 한다. 즉, 함수의 실행 컨텍스트가 종료되어도 해당 변수를 계속 참조할 수 있게 해준다. 클로저는 내부 함수가 외부 함수의 변수에 접근할 때 형성된다. 내부함수는 외부함수가 실행될 때 생성된 실행 컨텍스트의 변수를 기억하여 데이터를 은닉하고 캡슐화하는데 활용이 된다.
함수의 반환으로 클로저 구현하기
함수 안에서 다른 함수를 반환 함으로써 생성한다.
내부함수에서 외부함수의 변수에 접근할 수 있게 되면서 클로저가 탄생한다.
function createCounter() {
let count = 0;
// 반환되는 익명함수가 closure
// closure 덕분에 count 변수는 상태를 유지하고
// 익명함수 내부에서 createCounter의 지역변수인 count에 접근 가능
return function() {
count += 1;
return count;
}
}
const myCounter = createCounter();
// myCounter 함수는 closure 호출
// closure을 통해 createCounter 함수 내부의
// count 변수가 myCounter 함수 호출 사이에 상태를 유지
console.log(myCounter()); // 1
console.log(myCounter()); // 2
console.log(myCounter()); // 3
클로저는 외부 함수에서 선언된 변수들을 기억하고 이 변수들에 외부 함수가 종료된 후에도 접근 가능하다. createCounter가 반환하는 내부 함수가 바로 클로저이고 클로저는 count 변수에 접근하여 +1 씩 증가 시킨다. 이때 count 변수는 자유 변수에서 닫힌 변수로 변환된다. (클로저에 의해 열린 람다식에서 닫힌 람다식으로 변환) ⇒ count는 createCounter함수의 지역변수로!
일반적으로는 함수의 실행 컨텍스트가 종료되면 더이상 접근할 수 없다. 그러나 createCounter함수가 내부함수를 반환하면 이 내부함수는 count 변수에 대한 참조를 유지하여 count 변수는 클로저에 의헤 capture 된 변수가 되며, 가비지 컬렉터에 의해 수집되지 않게 된다. 따라서 createCounter 함수의 호출이 끝난 후에도 내부 함수를 통해 count 변수에 계속해서 접근하고 업데이트 가능하다.
콜백함수로 클로저 구현하기
클로저는 내부함수가 외부함수의 변수에 접근할 수 있게 하는 기능이다. 내부함수를 리턴하면 클로저가 생성된다. 리턴하는 내부함수는 콜백함수 형태로도 전달될 수 있다.
function createCounter(callback) {
let count = 0;
return function () {
count += 1;
callback(count);
}
}
const myCounter = createCounter(function (currentCount) {
console.log('현재 카운트: ', currentCount);
});
myCounter();
클로저와 콜백 함수 사용을 통해 상태를 유지하고 상태 변경 시마다 사용자가 정의한 동작을 수행하는 유연한 방식을 제공한다. createCounter를 사용할 때 callback함수를 인자로 제공한다. 이 callback 함수는 createCounter가 반환하는 내부 함수에 의해서 호출되며 currentCount 라는 매개변수로 count 값을 받는다. 콜백 함수를 사용한 클로저의 구현은 기존 클로저의 코드를 크게 변경하지 않으면서, 추가적인 기능을 확장시킬 수 있다.
객체 메서드로 클로저 구현
함수의 return으로 함수가 아닌 객체 리터럴 선언
function createCounter() {
let count = 0;
return {
increment: function () {
count += 1;
return count;
},
getCurrentCount: function () {
return count;
}
}
}
// myCounter 객체는 독립적인 count 값을 유지
const myCounter = createCounter();
myCounter.increment();
myCounter.increment();
console.log(myCounter.getCurrentCount());
increment 메서드와 getCurrentCount 메서드는 createCounter의 지역변수인 count에 접근한다. 두 메서드는 클로저를 형성하여 createCounter 함수의 실행 컨텍스트가 종료된 후에도 count 변수에 계속 접근 가능하다. 내부 함수가 객체 형태로 외부에 노출되고 외부 함수의 변수에 접근하면 클로저가 형성된다. 클로저를 사용해서 각 createCounter 함수 호출이 독립적인 count 상태를 유지할 수 있다. 각 myCounter 객체는 자신만의 count 값을 가지며 이는 다른 count 객체와 독립적이다.
createCounter(); 로 다른 독립적인 인스턴스 생성
// myCounter와는 독립적인 인스턴스
const myCounter2 = createCounter();
myCounter2.increment();
myCounter2.increment();
console.log(myCounter2.getCurrentCount());
클로저의 형성은 변수를 은닉하고 상태를 유지하는데 매우 중요하다.
클로저 사용 시 유의 사항은 외부 함수가 종료된 후에도 메모리에 남아서 과도한 사용 시 메모리 누수를 일으킬 수 있다. 또한 코드 복잡성이 증가할 수 있다.
클로저를 사용하는 이유
함수형 프로그래밍에서 중요한 역할을 하며, 특히 변수의 유효 범위와 데이터 캡술화등 이점을 제공한다. 클로저를 통해 외부함수의 변수 기억, 활용할 수 있어 React에서 클로저는 상태관리와 라이프 사이클 관리 등 중요한 역할을 한다.
<클로저를 사용하는 주요 이유>
- 캡슐화
- 데이터와 데이터를 조작하는 함수를 하나의 단위 (클래스나 모듈) 같은 구조 안에 묶는 기법
- 클로저는 자바스크립트에서 캡슐화를 구현하는 방법이다.
- 특정 함수를 통해서만 데이터에 접근하도록 제한한다. ex) counter 함수는 내부 count 변수를 가지고 이 변수는 외부에서 직접 접근 불가 count 값을 변경, 조회하기 위해서는 특정 함수에 접근 해야 함
- 데이터 은닉 구현 세부 사항을 숨기고 사용자에게 필요한 기능만 제공한다. 프로그램의 안전성을 높이고 사용자의 실수로부터 중요한 데이터를 보호한다. 클로저는 외부 스코프로 부터 독립된 변수를 가질 수 있어서 외부로 부터 변수 보호 데이터 직접 변경과 무분별한 변경 방지
- 상태 유지
- 어떤 데이터의 현재 상태 값을 시간이 지나도 유지
- 함수가 생성될 때 환경을 기억하고 이를 통해 해당 환경의 변수들의 상태를 유지한다.
- 클로저는 이런 변수들을 자신의 스코프 내에 안전하게 보관하며 외부의 영향으로 부터 독립적으로 존재할 수 있도록 한다.
- counter함수는 여러 번 호출되어도 이전의 count 상태를 유지하여, 그것을 기반으로 연산
- 상태 유지 특성을 통해 커링 함수, 지연 함수 실행 가능
대표 예시
- 커링 함수
- 커링은 함수의 부분적용과 지연 실행을 가능하게 해주는 기법
- 함수의 인자를 나눠 받으며 각 단계에서 클로저 생성하여 이전 단계의 인자를 기억한다.
[참고] 커링함수 커링: 여러 인자를 받는 함수를 단일 인자를 받는 함수로 변환하여 각각의 인자를 순차적으로 받는 연속된 함수의 체인으로 만든다. 커링된 함수는 하나의 인자를 받고 그 인자를 기억하며 다음 인자를 받기 위한 새로운 함수를 반환하고 그 과정은 모든 인자가 제공될 때까지 계속 된다. 함수의 재사용성과 모듈성을 높일 수 있고 함수의 지연 실행 및 부분 적용을 가능하게 한다. => 반복되는 인자를 가진 함수 호출에서 유용
예제 코드) 커링 함수의 지연 실행
필요할 때까지 계산을 연기하는 것이 지연 실행이다. 커링에서는 함수가 모든 인자를 받을 때까지 실행 지연 시킨다. 커링된 함수는 여러 인자를 받는 함수를 인자를 하나씩 받는 여러 함수의 연쇄로 변환한다. 각 중간 단계 함수는 다음 인자를 기다리며 실행 지연시킨다.
function add(a) {
return function (b) {
return function (c) {
return a + b + c;
}
}
}
// 함수가 실행되지 않음
// add 함수가 3개의 인자를 기대함, 3개 인자가 모두 제공될 때까지 대기
// 지연실행!!!! 함수가 모든 필요한 인자를 받을 때까지 실행을 지연시킴
console.log(add(1)(2));
// 함수 실행
console.log(add(1)(2)(3));
지연 실행을 통해 함수의 인자를 보다 유연하게 관리하게 한다. 함수의 인자를 부분적으로 적용하고 나머지 인자는 나중에 적용한다.
실제 사용 예시) 리덕스의 middleware 구조: 커링과 지연 실행 활용
리덕스 미들웨어에서 커링과 지연실행을 기반으로 강력한 패턴을 제공한다. 여러 단계의 함수를 통해 각각의 인자를 차례대로 받는다. 각 단계 함수는 다음 단계 함수를 반환하고 각 단계 함수는 실제 해당 단계 인자가 제공될 때까지 실행 지연한다.
const middleware = store => next => action => {
// 미들웨어 로직
}
- 1단계 함수 : store (리덕스 스토어를 매개변수로 받음) 미들웨어가 현재 상태를 조회하거나 새로운 액션 디스패치함
- 2단계 함수 : next 를 매개변수로 받음. next는 action을 다음 미들웨어로 전달하거나 middleware 체인의 마지막에 도달했다면 리듀서로 전달함
- 3단계 함수: 실제 dispatch 된 action을 매개변수로 받음. 미들웨어는 필요한 로직을 수행하고 액션을 수정하거나 추가적인 액션을 dispatch 혹은 무시함
- React의 useState와 useEffect
리액트에서 클로저는 hook과 관련하여 자주 언급이 된다.
- useState hook에서 사용되는 클로저의 특성
function createUseState() { let state; // 클로저를 통해 참조중인 state 변수의 상태를 변화 시키고 그 이후 렌더링 function setState(newState) { state = newState; render(); } function useState(initialState) { state = state === undefined ? initialState : state; return [state, setState]; } // useState 를 반환하므로 클로저가 형성됨. // closure에 의해 state 계속 참조할 수 있음 return useState; }
함수 컴포넌트는 상태가 변경될 때마다 새로운 인스턴스를 생성하기 떄문에 초기화된 상태만 가질 수 있었는데, 이러한 클로저를 사용하여 만들어진 hook을 사용하면 함수 컴포넌트 내에서 상태를 선언하고 관리를 할 수 있다. 함수 컴포넌트가 다시 실행되어도, 해당 함수의 상태값이 초기화되지 않고 React에 의해 사라지지 않는다.
클로저는 함수가 선언될 때의 환경을 기억하며, 이를 통해 함수가 자신의 외부 스코프에 있는 변수에 접근 가능하도록 한다. 이러한 특성을 이용해 상태를 유지하고 업데이트 하는 useState hook을 만들 수 있다. - useEffect의 dependency array와 관련된 클로저의 특성
클로저는 자신이 생성될 때 환경과 상태를 기억하는 함수인데, useEffect 콜백이 컴포넌트의 렌더링 시점에서 상태와 props를 캡처할 수 있도록 한다. 따라서 의존성 배열에 상태를 명시하지 않는다면 상태가 변경되어도 useEffect의 callback함수는 변화를 인지하지 못하고 초기값 만을 참조하게 된다. 즉, 의존성 배열에 특정 상태를 추가하면, 해당 상태가 변경될 때마다 useEffect callback 함수가 다시 생성되고 이를 통해 콜백 함수는 항상 최신 상태값을 기억하게 된다.
useEffect의 콜백함수는 클로저로 구현이 되어있다. useEffect의 의존성 배열을 통해 클로저의 특성을 적절히 관리할 수 있다. useEffect의 의존성 배열은 클로저 트랩을 피하기 위해서 명시하는 것이다.
[참고]
패스트캠퍼스 강의 (내 생에 마지막 JavaScript : 기초 문법부터 실무 웹 개발까지 한 번에 끝내기 초격차 패키지 Online) 를 기반으로 작성하였고, 추가 서치 내용을 정리하였습니다.
'개발공부 > Javascript' 카테고리의 다른 글
[JS] 이벤트 버블링과 캡처링 (0) | 2024.04.04 |
---|---|
[JS] 호이스팅 (0) | 2024.03.28 |
[JS] 기본형 데이터와 참조형 데이터 (0) | 2024.03.24 |
[JS] JavaScript 프로젝트에서 스타일 설정 방법 3가지 코드 예시 (1) | 2024.03.18 |
[JS] 실행 컨텍스트 (1) | 2024.03.13 |