본문 바로가기
Development

[21.01.16] YDKJSY - Appendix A: Exploring Further

by igy95 2024. 1. 7.

Implied Scopes

스코프는 가끔 관찰되지 않는 영역에서 생성된다. 이 스코프는 프로그래밍을 할 때 상식 선에서 코드를 작성한다면, 프로그램에 거의 영향을 끼칠 일은 없겠지만 그럼에도 불구하고 알 만한 가치가 있는 지식이 되어줄 것이다.

 

  • Parameter scope
  • Function name scope

Parameter Scope

함수의 매개 변수는 기본적으로 함수 내의 지역 변수와 동일한 스코프를 공유할 수 있다 볼 수 있지만, 매번 그렇지는 않다. 단순한 매개 변수의 선언인 경우 이 표현이 허용될 수 있다. 하지만 default parameter(a = b), rest parameter(...args), destructured parmeter({a, b}) 처럼 조금 복잡하게 인용된 매개 변수로 선언될 경우, 매개 변수들은 자신만의 고유의 스코프를 가지게 된다. 이를 증명할 수 있는 여러 예시들이 있지만 대부분 예시를 위한 예시일 뿐이지, 실용적이지는 않기 때문에 아래의 두 가지만 기억하면 된다.

 

  • 함수의 지역 변수로 매개 변수를 쉐도잉 하지 말 것.
  • parameter scope 안의 다른 매개 변수에 대한 클로저 함수를 default parameter 로 생성하지 말 것.

Function Name Scope

함수 표현식으로 함수를 선언하게 될 때, 변수명 말고 함수 자체에서 이름 식별자를 선언해줄 수 있다. 이러한 경우, 이 식별자는 할당된 변수의 스코프 혹은 자신의 함수 내부 스코프 중 어느 곳에도 속해 있지 않으며 이 또한 자신만의 스코프를 생성하게 된다. 결국 이러한 지식도 극히 드문 에러 케이스 방지를 위해서는 함수 내부의 지역 변수로 함수 이름을 쉐도잉하지 않는 것만 알면 된다.

Anonymous vs Named Functions

함수는 익명보다는 네임드, arrow function 보다는 regular function 을 쓰는 것이 권장된다. 그에 대한 이유들은 다음과 같다.

Explicit or Inferred Names?

함수의 이름을 익명으로 처리하느냐, 시의적절한 이름을 지어주느냐에 대한 차이는 디버깅 과정에서 크게 달라진다. 어느 코드에서 에러가 났을 때, 엔진은 stack 에 쌓인 호출 순서를 기준으로 '어느 함수에서 호출한 어느 함수에서 호출한 어느 함수...' 와 같은 메시지를 보내게 된다. 이 때, 내가 호출한 함수의 이름을 바로 확인할 수 있고, 그 함수의 이름이 적절하게 선언되었다면 메시지만으로도 논리의 흐름을 유추할 수 있으므로 디버깅이 훨씬 빨라진다.

Who am I?

렉시컬한 이름 식별자 없이는, 함수는 자신을 내부에서 참조할 수 있는 방법이 없다. 이것은 재귀나 이벤트 핸들링에서 특히 중요하기 때문에 이름을 써주는 것이 중요하다.

Names are Descriptors

함수가 가진 이름 자체가, 개발자 상호 간에 코드에 대한 논의가 이루어질 때 해당 코드의 설명을 용이하게 한다. 목적을 가진 함수는 그 자체로 어떤 역할을 수행하는 지 알 수 있고, 해당 비즈니스 로직에 대하여 이름이 가진 의도를 추론하며 따라갈 수 있기 때문에 단순히 이름을 안 쓰는 것에서 얻을 수 있는 공간적 효율성은 이름이 가진 가독성에 비하면 너무나 미미한 효과인 것이다.

 

코드에 대해 저자와 독자가 있다고 생각해 보자. 내가 저자라면 나의 코드에서 이름을 추가하기 전에 몇 번이나 이 함수의 목적에 대해 생각할까? 거의 한 번, 많아 봐야 두 세 번이다. 하지만 내가 독자라면, 하나의 함수를 읽어야하는 횟수는 가늠할 수 없다. 때문에 이름의 유무는 독자 입장에서는 꽤나 큰 부분이기 때문에 이름의 중요성을 간과해서는 안되는 것이다.

Arrow Functions

arrow function 은 항상 익명 함수다. 그렇기에 regular function을 사용할 수 있는 지점에서 굳이 arrow function 을 쓰는 것은 좋지 않은 생각이다. 물론 간결성을 더 향상 시킬 수야 있겠지만, 불필요한 간결성은 다른 독자들에게, 후에 코드를 읽을 본인에게 빠르게 코드를 이해할 수 있는 기회를 앗아갈 수 있다.

 

반면, 이 챕터의 범위에서는 벗어나지만 arrow function 이 다루고 있는 this의 존재는 신경 써야하는 부분이다. 다른 글에서 언급했다시피, 해당 함수 안에서 this는 여느 지역 변수들과 같이 상위 컨텍스트를 참조하는 lexical한 성격을 띄고 있다. 만약 코드 상에서 this가 무조건 외부 컨텍스트를 참조해야하기 때문에 var self = this.bind(this)를 써야하는 상황이라면 그냥 arrow function 을 쓰자. 특정 문제에 대한 보완을 위해 출연한 것이 arrow function 이기 때문이다.

Hoisting: Functions and Variables

호이스팅은 종종 그 성격 때문에 JS 설계의 오점이라 평가 받지만, 호이스팅을 이용함으로써 취할 수 있는 이점들도 있다. 그 예시는 다음과 같다.

 

  • 실행가능한 코드를 우선으로, 함수의 선언은 나중으로
  • 변수 선언의 의미론적 위치

Function Hoisting

코드 상에서 특정 함수의 호출을 우선하고, 선언을 그 다음에 할 수 있다는 것은 (함수의 이름이 적절히 지어졌다는 전제 하에) 프로그램의 동작을 위해 어떤 동작들이 수행되는지 내가 1. 선택적으로 2. 위에서 아래로 코드를 읽으며 빠르게 판단할 수 있다는 장점이 있다. 이는 결국 가독성을 향상시키는 요인이 된다.

Variable Hoisting

일단 이 책의 저자도 var 에 대한 호이스팅은 대부분 좋지 않다는 것에 동의한다. 하지만 본인이 CommonJS module 에서 var 호이스팅이 필요했던 케이스에 대해 설명을 해주었는데, 별로 공감이 되지 않았기에 패스.

The Case for var

내가 봤을 때, 저자는 스스로 var의 수호자가 되기를 자처하는 것 같다. 저자가 가지고 있는 세 개의 선언자에 대한 태도를 내 관점에서 간단히 정리해 보았다.

 

  • varlet은 서로를 대체하기 위한 존재가 아닌 케이스 별 best를 선택할 수 있는 것이어야만 한다.
  • const는 극히 제한된 사용성을 가지고 있다.

일단 const를 먼저 얘기하자면, 저자는 const라는 이름에 걸맞지 않은 object type 내의 value의 변동 가능성에 대해 부정적인 입장을 가지고 있다. 그리고 어차피 letvar 또한 적절한 위치에서 선언하고 코드 내에서 재할당을 하지 않으면 되는데 굳이 const를 선언하는 것도 이해할 수 없다고 한다. 그래서 저자가 이 선언자를 쓸 땐 명확하게 상수의 성격을 가지고 있는 값들에 대해서만 선언을 해주는 것 같다. 그리고 varlet을 비교했을 때 각각의 허용 가능한 scope의 범위에 맞게 사용하는 것을 지향하고, var의 쓰임새가 돋보일 수 있는 여러 케이스를 인용하여 설명하여 주었다.

 

이 파트를 읽으며 const 에 대한 저자의 생각엔 어느정도 동조할 수 있었다. 하지만 'var를 쓰는 게 좋다'는 의견에 대해서는 뒷받침 되는 근거들이 개인적으로 공감을 이끌어내기엔 조금 부족하지 않았나 싶다. 그 이유를 들자면, 일단 지금까지 책을 읽은 바로 추론할 수 있는 저자의 태도는 '프로그래밍을 하며 마주칠 수 있는 거의 모든 케이스를 낱낱이 파헤쳐 보며, 용법이나 문법에 대해 단순히 예상 결과와 다르기 때문에 피하기 보다는, 왜 이런 결과가 발생하는지 알면서 각 케이스에 맞는 best를 사용하자'는 주의인 것 같은데, var가 적절한 예시 중 조금 억지스러운 부분도 있었던 것 같고 (예시로 50줄 이상의 긴 함수에서는 var의 재선언이 가능한 점이 독자의 상기를 도울 수 있다고 했는데 애초에 50줄 이상의 긴 함수를 짜는 것부터 지양해야하는 게 아닌가? 싶었다.) 대부분 프로그래밍은 팀 단위로 협업을 하며 수행할 텐데, 굳이 희귀 케이스에서 best approach를 취하기 위해 팀 전원에게 var의 사용법을 전파하고 매번 코드 리뷰를 할 때 varlet의 사용에 대해 토론할 수도 있는 상황들이 너무 소모적으로 느껴졌다.

 

게다가 const의 제한된 사용성 때문에 몇몇 케이스를 제외하고는 다른 선언자를 사용하는 저자의 관점을 취한다면, varlet도 비슷한 용도로 사용될 수 있고 let에 비해 var가 더 나은 선택이 될 수 있는 케이스가 실제 프로그래밍 상에서는 극히 적기 때문에 작업의 효율성을 위해서 let의 사용을 지향하는 것이 덜 모순적이지 않나 싶었다.

 

원래 letconst를 즐겨 쓰는 입장에서 저자가 var 를 곧잘 쓴다는 것은 이미 알고 있었기 때문에, 해당 파트를 마주하게 되면 찬찬히 읽어보고 선언자에 대한 내 생각을 다시 돌아보려 했었다. 그렇지만 저자의 생각을 읽은 뒤에도 내 생각은 아마 변하지 않을 것 같다.

What's the Deal with TDZ?

Where It All Started

TDZ 는 사실 const 로부터 온 개념이다. 일찍이 letconst의 개념이 생기기 시작했을 때, 협회에서는 이 선언자들의 호이스팅 가능 여부에 대해 토론했었다고 한다. 만약 두 선언자가 호이스팅이 되지 않았더라면 어떻게 되었을까?

let greeting = "Hi!";

{
    // what should print here?
    console.log(greeting);

    // .. a bunch of lines of code ..

    // now shadowing the `greeting` variable
    let greeting = "Hello, friends!";

    // ..
}

let이 호이스팅이 되지 않는다는 가정 하에 console.log(...)가 참조하는 변수는 상위의 greeting일 것이다. 하지만 이는 꽤나 직관적이지 않고 개발자들에게 혼동을 줄 가능성이 있다. 때문에 모든 선언자는 기본적으로 호이스팅이 가능하다.

하지만 let, const는 호이스팅으로 블록 상단에서 확인할 수는 있지만 선언 전에 해당 값의 참조나 할당이 불가능한데 이는 왜 그런 것일까?

{
    // what should print here?
    console.log(studentName);

    // later

    const studentName = "Frank";

    // ..
}

두 선언자는 선언부에 다다르기 전까지는 변수의 초기화가 이루어지지 않는다. 만약 초기화가 이루어진다면 해당 선언자로 선언된 변수는 undefined의 값을 가지게 되는데, 이는 실질적인 선언부에서 "Frank"가 할당되었을 때 사실상 값의 재할당이 이루어지는 것으로 보며 const의 역할 범위에서 벗어나게 된다는 것이다.

 

그래서 위의 두 문제 사이에서 일어나는 딜레마를 해결하기 위해, 일시적인 dead zone을 설정하게 되었고 그것을 TDZ라고 부르게 되었다. 하지만 let은 이러한 관점에는 부합하지 않지만 협회에서 개발자들로 하여금 좀 더 일관적인 관점을 위해, 혹은 불필요한 호이스팅을 막기 위해 똑같이 TDZ를 적용시켰다고 한다.