티스토리 뷰
[ 클로저란(Closure)? ]
클로저는 JS라는 언어에 제약된 개념이 아니라 `함수를 일급객체로 사용하는 모든 언어`에서 사용하는 특성이다.
클로저는 언어 자체만 보면 "폐쇄"의 의미를 가지고 있으며 접근할 수 있는 변수의 범위와 대상을 제한하는 느낌에서 closure이라고 하는 것이 아닌가 싶다.
A closure is the combination of a function and environment within which that function was declared. -MDN 클로저-
쉽게 표현하자면, 클로저는 `자신이 생성될 때의 환경(lexical environment)을 기억하고, 그를 사용하는 함수` 이다.
// 클로저 예시
function makeAddNumFunc(num) {
const toAdd = num;
return function (num) {
// 익명함수가 호출될 때 toAdd 변수를 기억하고 있는 거
return num + toAdd;
};
}
const add5 = makeAddNumFunc(5);
- 위의 예시처럼 생성되는 함수가 기억하는 환경에서 사용하는 변수들을 `free variables(자유 변수)`라고 부른다. => 해당 코드에서는 toAdd가 free variable
- 익명함수에서 toAdd 라는 변수에 접근하려고 하면 makeAddNumFunc가 호출될 때의 환경에서 정의된 toAdd에만 접근할 수 있다.
- 스코프 체인은 제일 가까운 스코프로부터 상위로 올라가므로 이미 제일 가까운 환경에서 toAdd라는 변수를 찾았으므로 그 외의 스코프로는 접근x
[ 클로저의 원리 ]
클로저는 본인이 생성될 때의 환경을 기억한다. 그리고 본인이 호출될 때 그 환경에 있는 변수들을 참조할 수 있게 된다.
여기서 환경이란 Lexical Environment를 의미한다.
- Lexical Environment란 실행 컨텍스트의 구성요소 중 하나로서, 식별자와 식별자에 바인딩된 값, 상위 스코프에 대한 참조를 기록하는 객체이다.
- 따라서, 클로저가 기억하는 것은 자신이 생성될 때의 lexical environment인 것이다.
- [부연 설명] 참조 카운트가 0이 되면, `가비지 컬렉팅`의 대상이되어 메모리에서 사라지는 자바스크립트의 특징상 실행컨텍스트가 제거되면 참조 카운트가 0이 되면서 lexical envrionment도 메모리에서 없어져야 정상이다. 하지만 실행 컨텍스트가 제거되더라도 그 함수에서 리턴한 클로저가 해당 lexical environment를 참조하고 있다면 이 lexical environment는 CG(가비지 컬렉팅)대상이 되지 않는다.
- 위의 원리로 클로저가 생성될 때의 lexical environment를 기억할 수 있는 것이고, 이미 제거된 실행 컨텍스트의 lexical environment는 클로저 말고는 접근할 수 있는 방법이 없으므로 `정보의 은닉`이라는 장점 또한 얻게된다.
- 함수는 원래 값을 기억할 수 없는데 마치 class의 private 변수처럼 값을 기억하도록 만드는 것이라고 클로저를 이해할 수 있다.
[ 클로저 활용 ]
// TODO: squareOf 함수는 제곱값을 리턴한다
function squareOf(num) {
// key : value (해시 테이블 구조)
//? 해시테이블이란 key, value로 데이터를 저장하는 자료구조 중 하나로 데이터를 빠르게 검색할 수 있는 자료구조이다.
const cache = {};
if(cache[num]) return cache[num];
// 이때, 이전에 연산한 값은 새로 연산하는 것이 아니라 cache에 저장해두고 값을 바로 return하도록 만듦
cache[num] = num * num;
console.log('cache', cache);
return cache[num];
}
squareOf(2);
- 위의 로직은 이상적으로 보이나 치명적인 로직 결함이 있다. 바로, 함수가 매번 초기화된다는 점이다.
- 원래 의도대로라면 cache객체 안에는 2값이 들어가 있어야 하는데 항상 {} 라는 빈객체로 초기화된다. 즉, 함수는 값을 기억할 수 없다는 뜻
- 개선하려면 어떻게 해야하는가? => 제일 간단하게는 cache 변수를 함수에서 빼서 분리하는 것이다. e.g) 전역스코프로 const cache = {}를 뺀다. 하지만 이 방법도 문제가 있다. 바로, 누구에게나 공개된 프로퍼티라 값이 수정될 수 있다는 점이다. 개발자 중 한명이 실수로 cache[2] = 8 이라고 조작하는 코드를 썼을 때, 우리가 원하지 않는 오류가 발생한다.
- 위와 같은 예시 상황에서 원하는 값을 기억하도록 하면서 정보를 외부환경으로부터 은닉하게 만들려면 "클로저"를 사용하면 된다.
위의 예시 코드를 클로저를 활용해서 리팩토링
function outer() {
const cache = {};
return function squareOf(num) {
console.log('cache', cache);
if (cache[num]) return cache[num];
console.log('calc?');
cache[num] = num * num;
return cache[num];
};
}
const squareOfWithCache = outer();
squareOfWithCache(2);
squareOfWithCache(4);
cache[2] = 8; // ReferenceError 전역스코프에 있는 변수가 아니라 outer의 스코프에 존재하는 변수이기 때문
// squreOf라는 클로저만 cache에 접근가능하다.
- 기존 squreOf 내부에 존재하던 cache 변수를 outer함수의 변수로 옮겨줬다. 이렇게 하면 글로벌 스코프가 아니라 cache값을 외부로부터 안전하게 보호할 수 있으며 cache에 누적된 값을 기억해둘 수 있다.
클로저의 활용 예시로 currying(커링) 기법을 들 수 있다. 자세한 내용은 잘 모르기 때문에 따로 공부하시길..
ref: https://ko.javascript.info/currying-partials
// 커링 예시 코드
const sum = (x) => (y) => x + y;
const sum5 = sum(5);
sum5(10);
즉시실행함수 표현(IIFE, Immediately Invoked Function Expression)
함수는 보통 선언부와 호출부를 나눠서 사용하게 되는데, 둘을 나누지 않고 바로 실행시키는 JS 표현 기법이 있다.
IIFE 함수 선언부를 () 소괄호를 통해 값으로 만들어주고 그 뒤에 ()소괄호를 붙여 호출해준다. 매개변수가 필요하면 뒤에 소괄호에 넣어 호출해주면 된다.
- 즉시 실행함수는 이름이 필요없기 때문에 생략해도 된다.
- 예시 코드를 만들어보면 아래와 같다.
(function (x, y) {
return x + y;
})(5, 5);
즉시실행함수 표현은 클로저를 사용할 때 자주 같이 사용되기 때문에 알아두면 좋다.
예를 들어, 클로저 예시 코드에서 outer 함수는 cache를 저장하기 위한 임의의 함수로 딱히 어떤 작업을 하고 있지않다. => 그렇기 때문에 outer 함수 부분을 즉시실행함수로 변경해서 사용하면 원하는 동작을 짧은 코드로 동일하게 구현할 수 있다.
// IIFE ver
const squareOfWithCacheIIFE = (function () {
const cache = {};
return function squareOf(num) {
console.log('cache', cache);
if (cache[num]) return cache[num];
console.log('calc?');
cache[num] = num * num;
return cache[num];
};
})();
squareOfWithCacheIIFE(5);
[ 클로저의 활용 예시 ]
- 상태 기억
- 상태 은닉
- 상태 공유 : 클래스와 유사하게 사용하는 용도인데 IIFE 와 클로저를 결합해서 클래스 문법을 대체하여 사용할 수 있다.
FE 개발자이면서 React 를 주 라이브러리로 사용하고 있다면, 잘 알아야 한다. useState, useRef, useEffect 등 React에서 제공하는 Hook들은 이전 상태값을 기억하고 반환하는 기능을 한다. 이 hooks들은 함수인데 어떻게 값을 기억할까? 결국 내부적으로 클로저를 활용하고 있다는 말이 된다.
[ useState, useEffect 만들어보기 ]
// TODO: useEffect, useState 간단히 직접 구현해보기
// 1. 클로저, IIFE 사용해서 return => 구조 분해 할당 후 export 까지
// ! 항상 동일한 순서로 hook들이 호출되어야 하기 때문에 최상위에서 호출해야 한다. => 원하는 값을 제대로 뽑아오게 보장하기 위함
import { render } from "../main";
// interface of useState
// [state, setState] 튜플형식으로 리턴
// state -> saved state || initialValue
// setState: (value) => void;
// saved state = value;
// re-render
//? key point: state는 여러번호출 될 수 있다. state가 여러개 => 배열(array)에 저장
// 함수가 호출될 때마다 값이 초기화되면 안되니까 바깥에(클로저활용) hooks라는 배열을 만들어서 저장
export const { useState, useEffect } = (function makeMyHooks() {
const hooks = [];
let hookIndex = 0; // 몇번째 배열에 어떤 값이 매칭되어있는 지 알아야 하기 때문에
function useState(initialValue) {
if (hooks[hookIndex] === undefined) {
hooks[hookIndex] = initialValue;
}
//initialValue가 함수이고 맨 처음 실행되는 거라면 함수 실행
//리액트에서 초기값으로 콜백함수를 허용하는데 연산이 크거나 복잡한 값을 최초 렌더링 첫번째에만 실행하고 그 뒤로는 저장된 값을 사용하게 한다.
if (typeof initialValue === "function") {
if (hooks[hookIndex] === undefined) {
hooks[hookIndex] = initialValue();
}
}
const state = hooks[hookIndex];
// setStaet 내부에도 클로저를 만들어서 이 함수에 해당하는 state값의 index를 고정시켜줘야 한다.
// setState는 아래 함수를 즉시실행(IIFE)시킨 리턴값
// 실행시점: useState가 호출될 때
const setState = (function () {
const currentIndex = hookIndex;
return function (value) {
console.log("currentIndex", currentIndex);
console.log("hookIndex", hookIndex);
hooks[currentIndex] = value;
//re-render, 그리고 re-render됐으면 hookIndex를 초기화
render();
//hookIndex를 초기화시켜주지 않으면 hookIndex의 값이 점점늘어난다
hookIndex = 0;
};
})();
hookIndex++;
return [state, setState];
}
// interface
// 클린업함수 제외
// effect함수, deps 배열
// isFirstCall -> effect();
// isDepsNotProvided -> effect();
// hasDepsChanged -> effect();
// useEffect에서 기억해야하는 값?
// -> 의존성 배열 안의 값!
function useEffect(effect, dpes) {
const prevDeps = hooks[hookIndex];
const isFirstCall = () => prevDeps === undefined;
const isDepsNotProvided = () => deps === undefined;
const hasDepsChanged = () =>
deps.some((dep, index) => dep !== prevDeps[index]);
// OR연산자는 앞에서 하나라도 true면 이후 것들은 평가하지 않음 위에 함수 지연평가를 같이 활용한 예
if (isFirstCall() || isDepsNotProvided() || hasDepsChanged()) {
effect();
}
// 검증이 다 끝나고 나서 deps 업데이트
hooks[hookIndex] = deps;
console.log("hooks", JSON.stringify(hooks));
hookIndex++;
}
return { useState, useEffect };
})();
- 내장 배열 메서드 중 some()함수의 콜백은 순회하는 배열의 값 중 하나라도 true를 리턴하면 true를 반환한다. (forEach, map 과 비슷)
- 함수 지연 평가 개념 잘 활용하면 좋음 => 변수로 평가해도되긴하는데 함수로 만들어쓰는 건 유연성이 더 높을수도.. 개인 취향에 따라서 하면 된다.
- 클로저는 IIFE 형태로 많이 사용되는 듯.. 함수에서 값을 기억하려면 클로저를 활용하자, 함수가 호출되는 시점과 초기화되는 시점 등 잘 알고 써야 할 듯
예시 중 hooks를 크롬에서 찍어볼 때, 값이 null로 뜨는데 막상 열어보면 값이 찍혀있는 경우가 있음 그래서 JSON.stringify를 통해서 콘솔에 찍어봄 이 부분은 크롬 개발자들이 의도한거라 자세한 건 찾아봐야 할듯
'Frontend > JavaScript' 카테고리의 다른 글
[JavaScript | ES6 ] get(접근자), set(설정자), 프로퍼티 플래그와 설명자 (0) | 2023.07.24 |
---|---|
[JavaScript | ES6] Class (this, public & private 필드, 상속) (0) | 2023.07.18 |
[JavaScript] Array.from + selectedOptions 으로 선택된 요소의 값만 담은 배열반환하기 (0) | 2023.05.11 |
[JavaScript] 이중부정 연산자(!!) (0) | 2023.04.17 |
[node.js] process.env 환경변수 설정과 dotenv 라이브러리 (0) | 2023.04.13 |
- Total
- Today
- Yesterday
- D 플래그
- is()
- 형제 요소 선택자
- grid flex
- 항해99추천비추천
- Prittier
- nvm경로 오류
- 항해99프론트
- getStaticPaths
- 원티드 프리온보딩 FE 챌린지
- reactAPI
- nvm 설치순서
- 타입스크립트 DT
- 원티드 3월 프론트엔드 챌린지
- float 레이아웃
- fs모듈 넥스트
- 틸드와 캐럿
- 프리온보딩 프론트엔드 챌린지 3월
- 원티드 FE 프리온보딩 챌린지
- ~ ^
- getServerSideProps
- 항해99프론트후기
- 부트캠프항해
- && 셸 명령어
- 타입스크립트 장점
- text input pattern
- 원티드 프리온보딩 프론트엔드 챌린지 3일차
- tilde caret
- 프리렌더링확인법
- aspect-ratio
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |