본문 바로가기

웹 개발/프론트엔드 - JavaScript

[JavaScript] 한 달 동안 자바스크립트 박살내기 (9) - closure

「한 달 동안 자바스크립트 박살내기」는, ChatGPT쨩이 선생님이 되어, 매일 수업해주는 느낌으로 진행합니다.

GPT쨩의 말은 이렇게 작성합니다.

 

 

 

오늘 공부 내용 및 목표

 

오늘 학습 목표

오늘은 JavaScript에서 난도가 한 단계 올라가는 날이야. 핵심은 함수가 끝난 뒤에도, 그 함수가 만들어질 때 보던 바깥 변수에 접근할 수 있는 이유를 이해하는 거야.

closure는 이벤트, callback, 상태 유지, 모듈 패턴, 레거시 프론트엔드 코드를 읽을 때 정말 자주 만나게 돼. 오늘은 용어부터 차근차근 정의하고, factory → private state → callback → 참조 수명 순서로 연결해보자.

오늘의 2강째. 중요한 개념을 배우는 모양이다.

 

오늘의 위치

단계: JavaScript 핵심
주제: closure — lexical environment, factory/private state, callback/event, 참조와 수명
필수 예상시간: 약 110~120분
선택 심화: 15~20분

 

 

 

어제 내용 복습

 

복습 1

const base = {  
 run() {  
   console.log("base run");  
 },  
};  
  
const player = Object.create(base);  
player.name = "Gran";  
  
player.run();
player 객체 자체에는 run이 없는데도 왜 실행될까?

player는 base를 프로토타입으로 해서 만들어진 객체이다. 그래서 run()을 실행하면 player에 run이 있는지 체크해, 있으면 그걸 실행하고, 없으면 프로토타입으로부터 찾아서 실행하기 때문에 base에 정의된 run이 실행된다.

정답.

 

 

복습 2

function getAction() {  
 return function () {  
   console.log("run");  
 };  
}  
  
const action = getAction();
action에는 무엇이 저장될까?

console.log("run")을 실행하는 함수가 저장된다.

정답.

 

 

복습 3

function outer() {  
 const x = 10;  
  
 function inner() {  
   console.log(x);  
 }  
  
 inner();  
}
x는 어떤 scope의 변수일까?

outer 함수 내에 정의된 변수이다.

정답. 정확히는, outer 함수의 local scope에 있는 변수이다.

 

 

 

핵심 수업

 

lexical environment - 코드가 만들어진 위치를 기준으로 접근 가능한 변수 정보를 묶어 관리하는 구조

lexical environment(렉시컬 환경)는 JavaScript가 코드 실행 중에 “이 위치에서 어떤 변수 이름에 접근할 수 있지?”를 관리하기 위한 내부적인 환경이야.

우리가 실제 코드에서 lexical environment라는 객체를 직접 만들지는 않아. JavaScript 엔진이 scope와 변수 접근을 처리하기 위해 내부적으로 관리한다고 보면 돼.

음, lexical scope와 관련있는 개념인거같다.

 

const globalValue = 1;  
  
function outer() {  
 const outerValue = 2;  
  
 function inner() {  
   const innerValue = 3;  
   console.log(globalValue, outerValue, innerValue);  
 }  
  
 inner();  
}  
  
outer();
inner 안에서는 innerValue, outerValue, globalValue 순으로 바깥쪽까지 찾아갈 수 있어.
중요한 건 함수가 어디에서 호출됐는지가 아니라, 어디에서 정의됐는지가 변수 탐색의 기준이 된다는 거야.

어떤 함수 내에서는, 그 함수 밖에서 정의된 변수도 참조할 수 있다.

우리 반 안에선 우리 반의 규칙이 적용되지만, 학교 전체의 규칙도 적용되고, 학교가 소속된 그 지역의 규칙도 적용되는 것과 마찬가지.

 

const value = "global";  
  
function outer() {  
 const value = "outer";  
  
 return function inner() {  
   console.log(value);  
 };  
}  
  
const fn = outer();  
fn();

이 경우에도 출력은 outer다. 

 

 

closure - 함수가 자신이 만들어질 때 접근 가능했던 바깥 변수를 기억하고 계속 접근할 수 있는 성질

closure(클로저)는 함수가 자신이 생성된 lexical environment를 기억해서, 그 함수가 나중에 다른 곳에서 실행돼도 당시 바깥 변수에 접근할 수 있는 성질을 말해.
function outer() {  
 const message = "hello";  
  
 return function inner() {  
   console.log(message);  
 };  
}  
  
const fn = outer();  
fn();
outer()가 끝난 뒤에도 message를 사용할 수 있는 이유가 바로 closure야.
개념적으로는 이렇게 잡으면 좋아.

함수 코드 + 그 함수가 만들어질 때 접근 가능했던 바깥 변수 환경

그런데, closure가 "값을 복사해서 저장"하는 게 아니라는 걸 조심해야해.
function createCounter() {  
 let count = 0;  
  
 return function () {  
   count += 1;  
   console.log(count);  
 };  
}  
  
const counter = createCounter();  
  
counter();  
counter();  
counter();

음... 이러면 1만 3번 출력되나?

싶었는데, 1,2,3이 출력된다.

"값을 복사해서 저장"하는게 아니라, 그냥 같은 변수에 접근하는 것이기 때문이다.

 

 

factory function - 호출할 때마다 새 함수나 객체를 만들어 반환하는 함수

factory function(팩토리 함수)은 호출할 때마다 새로운 함수나 객체를 만들어 반환하는 함수야.
function createCounter() {  
 let count = 0;  
  
 return function () {  
   count += 1;  
   return count;  
 };  
}  
  
const counterA = createCounter();  
const counterB = createCounter();  
  
console.log(counterA()); // 1  
console.log(counterA()); // 2  
console.log(counterB()); // 1
각 호출은 서로 다른 lexical environment를 만들어.

이 부분에 대해서 추가 질문을 했는데, 

closure을 반환하는 함수는, 그 함수가 호출될 때마다 고유의 lexical environment를 만들기 때문에, counterA와 counterB의 lexical environment가 다르게 생성되어, 변수 count도 각각 별개의 것으로 취급된다.

 

 

private state - 외부에서 직접 접근하지 못하고 특정 함수만 접근할 수 있게 숨긴 상태

private state(외부에서 직접 접근하지 못하게 숨긴 상태)는 상태값을 외부 코드가 마음대로 바꾸지 못하게 하고, 정해진 함수만 읽거나 변경하게 하는 구조야.
function createProgress(initialValue) {  
 let value = initialValue;  
  
 return {  
   increase(amount) {  
     value += amount;  
   },  
  
   getValue() {  
     return value;  
   },  
 };  
}  
  
const progress = createProgress(100);  
progress.increase(30);  
  
console.log(progress.getValue()); // 130  
console.log(progress.value); // undefined

아마 이게 JS에서 캡슐화를 하는 방법인걸까?

createProgress의 반환 객체에 value라는 프로퍼티가 없기 때문에, progress.value로 직접 접근을 하는건 불가능하지만, progress.getValue()를 호출하면 closure을 통해 접근하는게 가능하다.

 

 

callback과 closure

callback은 다른 함수에 전달되어 나중에 호출되는 함수야.
function createHandler(name) {  
 return function () {  
   console.log(`${name} selected`);  
 };  
}  
  
const selectGran = createHandler("Gran");  
const selectDjeeta = createHandler("Djeeta");  
  
selectGran();  
selectDjeeta();
각 함수는 자신이 생성될 때의 name이 들어 있는 환경을 기억하고 있어.

createHandler는 함수를 반환하는 factory function인데, selectGran에는 name이 Gran인 반환 함수가 저장되고, selectDjeeta에는 name이 Djeeta인 반환 함수가 저장된다.

 

 

 

코드 보고 동작 예측하기

 

예측 1

function createCounter() {  
 let count = 0;  
  
 return function () {  
   count += 1;  
   return count;  
 };  
}  
  
const counter = createCounter();  
  
console.log(counter());  
console.log(counter());

counter에 createCounter()가 대입되는 시점에서, counter는 하나의 lexical environment를 가진다.

그래서 1, 2가 출력된다.

정답.

 

 

예측 2

function createCounter() {  
 let count = 0;  
  
 return function () {  
   count += 1;  
   return count;  
 };  
}  
  
const a = createCounter();  
const b = createCounter();  
  
console.log(a());  
console.log(a());  
console.log(b());

여기선 a와 b가 각각의 lexical environment를 가진다.

그래서 1, 2, 1이 출력된다.

정답.

 

 

예측 3

let message = "global";  
  
function createPrinter() {  
 let message = "local";  
  
 return function () {  
   console.log(message);  
 };  
}  
  
const print = createPrinter();  
message = "changed global";  
  
print();

print에는, local을 출력하는 함수 자체가 저장된다. 그래서 local이 출력된다.

정답.

 

 

예측 4

function createState() {  
 let value = 10;  
  
 return {  
   increase() {  
     value += 5;  
   },  
  
   read() {  
     return value;  
   },  
 };  
}  
  
const state = createState();  
  
state.increase();  
state.increase();  
  
console.log(state.read());

state에는 increase()랑 read()가 들어있는 객체가 저장되고, 각 메소드의 value에는 10이 저장되어있다.

state.increase가 두 번 호출되면, value에 5를 두 번 더하니 value에 20이 들어간다.

출력도 20.

정답.

 

 

 

직접 코딩

 

factory function 만들기

createSelectionCounter라는 factory function을 만들어보자.
function createSelectionCounter() {  
 // TODO: 외부에 직접 노출하지 않을 상태 만들기  
  
 return {  
   select() {  
     // TODO  
   },  
  
   getCount() {  
     // TODO  
   },  
 };  
}  
  
const counter = createSelectionCounter();  
  
counter.select();  
counter.select();  
  
console.log(counter.getCount()); // 2가 나와야 함

뭔가 점점 과제 설정을 대충 하는거 같은 느낌?

아래와 같이 작성해주었다.

 

function createSelectionCounter() {
  let count = 0; 

  return {
    select() {
      count += 1;
    },

    getCount() {
      return count;
    },
  };
}

const counter = createSelectionCounter();

counter.select();
counter.select();

console.log(counter.getCount());

 

 

코드 읽기 / 디버깅 드릴

function createHandlers() {  
 let currentName = "";  
  
 const handlers = [];  
  
 currentName = "Gran";  
 handlers.push(function () {  
   console.log(currentName);  
 });  
  
 currentName = "Djeeta";  
 handlers.push(function () {  
   console.log(currentName);  
 });  
  
 return handlers;  
}  
  
const handlers = createHandlers();  
  
handlers[0]();  
handlers[1]();
1. 실제 출력은 무엇일까?
2. 왜 첫 번째 함수가 "Gran"을 기억하지 않을까?

음? Gran을 기억하지 않나? 

아, createHandlers가 호출되는 시점에선 currentName이 Djeeta라서 Djeeta만 두 번 저장되나?

그렇다. handlers에 저장되는 두 함수는, 각각의 문자열 값을 복사해서 저장한 것이 아니라 같은 문자열 변수인 currentName을 참조하기 때문이다. 

 

 

 

종료 테스트

 

1번 문제

function createValue() {  
 let value = 1;  
  
 return function () {  
   value *= 2;  
   return value;  
 };  
}  
  
const fn = createValue();  
  
console.log(fn());  
console.log(fn());  
console.log(fn());

2, 4, 6이 출력된다.

fn에 createValue()가 저장되는 시점에서 lexical environment가 생성되고, 여러 번 호출될 때마다 같은 변수인 value를 조작하기 때문이다.

정답.

 

 

2번 문제

function createCounter() {  
 let count = 0;  
  
 return () => ++count;  
}  
  
const counterA = createCounter();  
const counterB = createCounter();
counterA와 counterB가 같은 count를 공유하지 않는 이유를 설명해봐.

각각 createCounter()가 대입되어 다른 lexical environment를 가지기 때문이다.

정답.

 

 

3번 문제

function createPlayer(name) {  
 let score = 100;  
  
 return {  
   add(amount) {  
     score += amount;  
   },  
  
   print() {  
     console.log(name, score);  
   },  
 };  
}  
  
const gran = createPlayer("Gran");  
const djeeta = createPlayer("Djeeta");  
  
gran.add(30);  
djeeta.add(10);  
  
gran.print();  
djeeta.print();

"Gran, 130", "Djeeta, 110"이 출력된다.

gran과 djeeta는 각각의 lexical environment를 가지고, 호출되는 시점에서의 name을 기억하여 각각 Gran과 Djeeta가 저장.

그리고 각각 30과 10이 더해지기 때문이다.

정답.

 

 

 

감상

 

어우 너무 헷갈린다.