본문 바로가기
JAVASCRIPT

[JS][함수 vs 블록 스코프] 스코프 역할을 하는 블록

by KBS 2022. 2. 17.
728x90

스코프 역할을 하는 블록

함수가 가장 일반적인 스코프 단위이자 현재 자바스크립트에서 통용되는 가장 널리 퍼진 디자인 접근법이기는 하지만, 다른 스코프 단위도 존재하고 이를 이용하면 더 좋은 깔끔한 코드를 작성할 수 있다.

자바스크립트를 제외하고도 많은 언어가 블록 스코프를 지원한다. 그래서 다른 언어를 사용하던 개발자들은 블록 스코프라는 개념에 익숙하지만, 자바스크립트만을 써왔던 개발자에게는 이개념이 약간은 어색할 수 있다.

그러나 아직 블록 스코프 방식으로 한 줄도 코딩해 본 적이 없더라도 다음과 같은 자바스크립트 코드는 매우 익숙할 것이다.

for (var i = 0; i < 10; i++) {
  console.log(i);
}

 

변수 i를 for반복문의 시작부에 선언하는 이유는 보통 i를 오직 for반복문과 관련해서 사용하려 하기 때문이다. 그러고는 변수 i가 실제로 둘러싼 스코프에 포함된다는 사실은 무시한다. 블록 스코프의 목적이 바로 이것이다. 변수르 최대한 사용처 가까이에서 최대한 작은 유효범위를 갖도록 선언하는 것 말이다.

var foo = true;

if (foo) {
  var bar = foo * 2;
  bar = something(bar);
  console.log(bar);
}

 

변수 bar는 오직 if문에서만 사용하므로, bar를 if 블록 안에 선언하는 것은 타당하다. 그러나 사실 var를 사용할 때 변수를 어디에서 선언하는지는 중요한 문제가 아니다. 선언된 변수는 항상 둘러싸인 스코프에 속하기 대문이다. 앞의 코드는 보기에만 스코프처럼 보이는 '가짜' 블록 스코프로, bar를 의도치 않게 다른 곳에서 사용하지 않도록 상기시키는 역할을 할 뿐이다.

블록 스코프는 앞서 언급한 '최소 권한 노출의 원칙'을 확장하여 정보를 함수 안에 숨기고, 나아가 정보를 코드 블록 안에 숨기기 위한 도구다.

예제를 다시 살펴보자.

var foo = true;

if (foo) {
  var bar = foo * 2;
  bar = something(bar);
  console.log(bar);
}

 

오직 for 반복문에서만 사용될 변수 i로 스코프 전체를 왜 오염시켜야 할까?

무엇보다 개발자들은 의도하지 않게 변수가 원래 용도 이외의 곳에서 (재)사용됐는지 점검하고 싶어 한다. 예를 들어, 정해진 장소 밖에서 변수가 사용되면 알려지지 않은 변수라는 오류가 발생한다는 식으로 말이다. 블록 스코프를 사용한다면 변수 i는 오직 for 반복문안에서만 사용할 수 있고, 이외 함수 어느 곳에서 접근하더라도 오류가 발생할 것이다. 이는 변수가 혼란스럽고 유지 보수하기 어려운 방식으로 재사용 되지 않도록 막는다.

그러나 슬픈 현실은 적어도 외견사으로 자바스크립트는 블록 스코프를 지원하지 않는다. 물론, 좀 더 파고들면 방법은 있다.

with

with는 이양해야할 구조이긴 하지만 블록 스코프의 형태를 보여주는 한 예로, with 문 안에서 생성된 객체는 바깥 스코프에 영향 주는 일 없이 with문이 끝날 때까지만 존재한다.

try/catch

잘 알려지지 않은 사실이지만, 자바스크립트 ES3에서 try/catch 문 중 catch 부분에서 선언된 변수는 catch 블록 스코프에 속한다.

try {
  undefined();
} catch (err) {
  console.log(err);
}
console.log(err); // ReferenceError : 'err' not found

예제에서 보듯 변수 err은 오직 catch 문 안에만 존재하므로 다른 곳에서 참조하면 오류가 발생한다.

catch 문의 블록 스코프 효과는 쓸데없고 학술적인 것처럼 느껴질 수도 있다.

이런 동작 은 명시되어 있고 실제 모든 표준 자바스크립트 환경에서 똑같이 작동하지만, 많은 린터가 여전히 같은 스코프 내에 같은 확인자 이름으로 오류 변수를 선언한 catch 문이 둘 이상 있을 경우 경고를 보낸다. 사실 이런 선언은 재정의가 아니다. 그 변수들은 안전하게 각기 다른 블록 스코프에 속하기 때문이다. 그래도 린터는 짜증나게 이부분을 계속 경고한다. 이 불필요한 경고를 피하고자 catch 변수 이름을 err1, err2와 같은 식으로 정하거나 그냥 린터에서 변수 이름 중복 확인 옵션을 꺼버리기도 한다.

 

let

지금까지 살펴본 자바스크립트의 블록 스코프 기능은 비주류 적인 요소를 통해서 구현된 것이다. 자바스크립트의 블록 스코프 기능이 이것뿐이었다면(사실 오랜 기간 동안 이것밖에 없었다) 자바스크립트 개발자들에게 블록 스코프는 별로 유용하지 않았을 것이다.

다행이도 ES6에서 이런 상황이 바뀌면서 새로운 키워드 let이 채택됐다. letvar같이 변수를 선언하는 다른 방식이다. 키워드 let은 선언된 변수를 둘러싼 아무 블록(일반적으로 {})의 스코프에 붙인다. 바꿔 말홰, 명시적이진 않지만 let은 선언한 변수를 위해 해당 블록 스코프를 이용한다고도 말할 수 있다.

var foo = true;

if (foo) {
  let bar = foo * 2;
  bar = something(bar);
  console.log(bar);
}

console.log(bar); // ReferenceError

 

let을 이용해 변수를 현재 블록에 붙이는 것은 약간 비명시적이다. 코드를 작성하다 보면 블록이 왔다 갔다 하고 다른 블록으로 감싸기도 하는데, 이럴 대 주의하지 않으면 변수가 어느 블록 스코프에 속한 것인지 착각하기 쉽다.

블록 스코프에 사용되는 블록을 명시적으로 생성하면 이런 문제를 해결할 수 있다. 변수가 어느 블록에 속했는지 훨씬 더 명료해지기 때문이다. 일반적으로 명시적인 코드가 암시적이고 미묘한 코드보다 낫다. 이런 명시적 블록 스코프 스타일은 쉽게 사용할 수 있고, 다른 언어에서 블록 스코프가 작동하는 방식과도 더 자연스럽게 만난다.

var foo = true;

if (foo) {
  // <- explicit block
  let bar = foo * 2;
  bar = something(bar);
  console.log(bar);
}

console.log(bar); // ReferenceError

 

그저 {} 를 문법에 맞게 추가만 해도 let을 통해 선언된 변수를 묶을 수 있는 임의의 블록을 생성할 수 있다. 앞의 코드에서 if문 안에 명시적인 블록을 만들엇다. 이렇게 하면 나중에 리팩토링 하면서 if 문의 위치나 의미를 변화시키지 않고도 전체 블록을 옮기기가 쉬워진다.

호이스팅은 선언문이 어디에서 선언됐든 속하는 스코프 전체에서 존재하는 것처럼 취급되는 작용을 말한다. 그러나 let을 사용한 선언문은 속하는 스코프에서 호이스팅 효과를 받지 않는다. 다라서 let으로 선언된 변수는 실제 선언문 전에는 명백하게 '존재'하지 않는다.

{
  console.log(bar); // ReferenceError
  let bar = 2;
}

 

가비지 콜렉션

블록 스코프가 유용한 또 다른 이유는 메모리르 ㄹ회수하기 위한 클로저 그리고 가비지 콜렉션과 관련 있다.

function process(data) {
  // ...
}

var someReallyBigData = {
  // ...
};

process(someReallyBigData);

var btn = document.getElementById("my_button");

btn.addEventListener(
  "click",
  function click(evt) {
    console.log("button clicked");
  },
  /*capturingPhase=*/ false,
);

 

클릭을 처리하는 click함수는 someReallyBigData 변수가 전혀 필요 없다. 다라서 이론적으로는 process()가 실행된 후 많은 메모리를 먹는 자료 구조인 someReallyBigData는 수거할 수도 있다. 그러나 (어떻게 구현됐는지에 따라 다를 수 있지만) 자바스크립트 엔진은 그 데이터를 여전히 남겨둘 것이다. click함수가 해당 스코프 전체의 클로저를 가지고 있기 때문이다.

블록 스코프는 엔진에게 someReallyBigData가 더는 필요 없다는 사실을 더 명료하게 알려서 이문제를 해결할 수 있다.

function process(data) {
  // ...
}
{
  let someReallyBigData = {
    // ...
  };

  process(someReallyBigData);
}

var btn = document.getElementById("my_button");

btn.addEventListener(
  "click",
  function click(evt) {
    console.log("button clicked");
  },
  /*capturingPhase=*/ false,
);

 

명시적으로 블록을 선언하여 변수의 영역을 한정하는 것은 효과적인 코딩 방식이므로 익혀두면 좋다.

let 반복문

let은 앞에서 살펴본 for 반복문에서 특히 유용하게 사용할 수 있다.

for (let i = 0; i < 10; i++) {
  console.log(i);
}

console.log(i); // ReferenceError

 

let은 단지 i를 for 반복문에 묶었을 뿐만아니라 반복문이 돌 때마다 변수를 다시 묶어서 이전 반복의 결괏값이 제대로 들어가도록 한다.

let 선언문은 둘러싼 함수 스코프가 아니라 가장 가까운 임의의 블록에 변수를 붙인다. 따라서 이전에 var 선언문을 사용해서 작성된 코드는 함수 스코프와 숨겨진 연계가 있을 수 있으므로 리팩토링을 위해서는 단순히 var를 let으로 바꾸는 것 이상의 노력이 필요하다.

const

ES6에서 키워드 let과 함께 const도 추가됐다. 키워드 const역시 블록 스코프를 생성하지만, 선언된 값은 고정된다. 선언된 후 const의 값을 변경하려고 하면 오류가 발생한다.

var foo = true;

if (foo) {
  var a = 2;
  const b = 3;
  a = 3;
  b = 4; // error
}

console.log(a); // 3
console.log(b); // ReferenceError

 

정리하기

짜바스크립트에서 함수는 스코프를 이루는 가장 흔한 단위다. 다른 함수 안에서 선언된 변수와 함수는 본질적으로 다른 '스코프'로부터 '숨겨진'것이다. 이는 좋은 소프트웨어를 위해 적용해야할 디자인 원칙이다.

그러나 함수는 결코 유일한 스코프 단위가 아니다. 블록 스코프는 함수만이 아니라(일반적으로 {} 같은) 임의의 코드 블록에 변수와 함수가 속하는 개념이다.

ES3부터 시작해서 try/catch 구조의 catch 부분은 블록 스코프를 가진다. ES6에서 키워드 let(키워드 var와 비슷하다)이 추가되어 임의의 코드 블록 안에 변수를 선언하 수 있게 되었다. if () { let a = 2; }에서 변수 a는 if문의 {} 블록 스코프에 자신을 붙인다.

쉽게 착각하지만, 블록 스코프는 var 함수 스코프를 완전히 대체할 수 없다. 두 기능은 공존하며 개발자들은 함수 스코프와 블록 스코프 기술을 같이 사용할 수 이서야 하고 그래야 한다. 상황에 따라 더 읽기 쉽고 유지 보수가 쉬운 코드를 작성하기 위해 두 기술을 적절한 곳에 사용하면 된다.


참고

  • You Don't Know JS - 타입과 문법, 스코프와 클로저( 한빛 미디어 )
728x90

댓글