기본값 매개변수(default parameter)와 스코프

김민재·2024년 1월 5일
0

의문의 시작

const test1 = () => {
  let count = 0;
  const inner = () => {
    console.log(count); // count: closure scope
    debugger;
  };
  inner();
};
test1();

const test2 = (initialValue = 0) => {
  let count = initialValue;
  const inner = () => {
    console.log(count); // count: block scope
    debugger;
  };
  inner();
};
test2();

의문의 시작은 위 코드에서 시작됐다. 두 함수는 Closure Scope를 사용하는 전형적인 모양을 취하고 있다. test1의 내부 함수 inner는 상위 Lexical Scope의 count 변수를 사용하기 위해 일반적으로 알려진대로 Closure Scope를 참조한다. 그런데 기본값 매개변수를 사용하는 test2의 경우 Block Scope를 사용한다. 이게 대체 어떻게 된 일일까?

일반적으로 알려진대로 count 변수가 Closure Scope에 선언되어 있다.

하지만 기본값 매개변수를 사용하면 count 변수가 Block Scope에 선언된다.

의문 해결하기

공식 문서 찾아보기

The default parameter initializers live in their own scope, which is a parent of the scope created for the function body.
출처: Mozilla 공식문서: Default parameters

기본값 매개변수에 대한 공식 문서를 찾아보면 위와 같은 설명이 나온다. 즉, 함수에 기본값 매개변수를 사용할 경우, 기본값 매개변수 이니셜라이저는 함수 body의 부모가 되는 자체적인 스코프를 갖는다.

자세히 살펴보기

const test3 = (initialValue = 0) => {
  let count = initialValue;
  // initialValue: local scope
  // count: block scope
  debugger;
};
test3();

공식 문서를 기반으로 test3의 스코프를 자세히 살펴보자.

처음에는 count는 함수의 실행 컨텍스트인 Local Scope에 선언되고, initialValue는 Local Scope의 상위 스코프에 초기화되어야 한다고 생각했다. 그런데 실제로는 Block Scope에 count가 선언되고 Local Scope에 initialValue가 초기화되어 있었다. 그렇다면 initialValue는 함수 내부 컨텍스트에 존재하면서 그 하위에 Block Scope로 기존 함수 body가 있다는 의미이다.

Local Scope의 상위 스코프에 initialValue가 초기화되는 것이 아니라, Local Scope에 initialValue가 초기화되고 그 하위 스코프인 Block Scope에 count가 선언된다.

흉내내기

const test4 = (initialValue) => {
  initialValue = 0; // local scope
  { // 기존 함수 body
    let count = initialValue; // block scope
    debugger;
  }
};
test4();

더 쉬운 이해를 위해서 test3 함수를 흉내낸 test4 함수를 작성해봤다. 함수의 Local Scope에서 initial value를 초기화를 하고, 기존의 함수 body는 Block으로 감싸서 count는 Block Scope에서 선언했다. 디버거를 통해 스코프 체인을 확인해보면 test3의 경우와 같은 것을 볼 수 있다.

test3의 스코프와 동일하다. 실제로 내부에 어떻게 구현되어 있는지는 모르겠지만, 이런 형태로 변환하니 확실히 이해가 됐다.

의문 해결

기존 코드 다시 보기

const test2 = (initialValue = 0) => {
  let count = initialValue;
  const inner = () => {
    console.log(count); // count: block scope
    debugger;
  };
  inner();
};
test2();

이제 test2 코드를 다시 보니 명확하게 보인다. initialValue는 Local Scope에 있고, count는 Block Scope에 있으니 count에 접근하려면 Block Scope를 참조하는 것이 당연하다.

응용하기

const test5 = (value = 5) => {
  let count = 0;
  const inner = () => {
    console.log(value); // closure scope
    console.log(count); // block scope
    debugger;
  };
  inner();
};
test5();

그렇다면 이 경우는 어떨까?

const test5 = (value) => {
  value = 5; // local scope
  {
    let count = 0; // block scope
    const inner = () => {
      console.log(value); // closure scope
      console.log(count); // block scope
      debugger;
    };
    inner();
  }
};
test5();

이해를 위해 아까처럼 흉내를 내면 이렇게 변환된다.

여기서 valuetest5의 Local Scope에 선언되어 있다. 따라서 inner에서 value를 참조하려면 test5의 Local Scope, 즉 Closure Scope를 참조해야 한다. inner에서 count를 참조할 때는 Closure Scope까지 두 단계 올라갈 필요 없이 바로 위의 Block Scope만 사용하면 된다.

count는 Block Scope, value는 Closure Scope에 선언되어 있다. 최종적으로 inner의 스코프 체인은 Local(inner), Block, Closure, Script, Global 순으로 연결된다.

0개의 댓글