본문 바로가기
이론/Frontend

JavaScript의 스코프 - 3 (Scope of JavaScript)

by 유세지 2021. 5. 13.

지금까지 공부한 스코프와 그 범위, 렉싱 단계 이후 스코프를 변경하는 방법들을 통해 스코프의 특징에 대해 어느정도 감이 잡히셨을거라 생각합니다. 그래도 아직은 약간 부족한 느낌이 드니 조금 더 확실히 해보겠습니다.

 

이전 글에서 스코프는 분명 서로 중첩되지 않는 독립적인 공간을 형성한다고 하였습니다. 예를 들어 설명했었던 함수가 이러한 스코프를 형성하는 역할을 하는 좋은 예시 중 하나입니다. '예시 중 하나' 라는 것은 다른 자료 구조 또한 스코프를 형성할 수 있다는 이야기겠지요? 이번 포스트에서는 스코프를 형성하는 구조들에 대해 알아보도록 하겠습니다.

 

 

함수 기반 스코프

가장 먼저 함수를 기반으로 한 스코프입니다. 함수 내에 속한 변수와 함수들은 중첩된 스코프들을 포함하여 함수 전체에 걸쳐 사용됩니다. 우리가 이미 자연스럽게 사용하고 있던 현재 스코프부터 상위 스코프에 있는 변수들을 이용할 수 있다는 접근법을 통해서도 알 수 있습니다.

 

이러한 특성을 응용하면 특정한 부분을 다른 접근으로부터 숨기는 행위가 가능해집니다. 함수 선언문을 통해 코드를 감싸게 되면 기존의 스코프에선 숨겨진 부분을 확인할 수 없게 됩니다. 실제로 코드가 사라진 것은 아니지만, 새로운 스코프를 생성함으로써 코드의 스코프가 새로 만들어진 함수로 바뀌는 것이죠.

 

이렇게 코드를 숨기는 기법은 리팩토링 스터디에서 비슷한 늬앙스로 이야기했던 최소 권한의 원칙을 적용하는데 유용합니다. 외부에서 접근해도 되는 데이터와 접근해서는 안되는 데이터들을 알맞게 배치하고, 필요없는 노출 자체를 줄여 위험한 코드의 생성을 막을 수 있게 됩니다.

 

굳이 코드의 안정성을 언급하지 않더라도, 중복되는 이름을 가진 확인자들을 분리하여 사용할 수 있다는 직관적인 장점도 있습니다. 지난 포스트에서 설명했던 가려짐 현상을 이용하여 같은 이름의 확인자를 서로 다른 값을 할당하여 사용할 수 있는데, 잘못 참조하게 되면 전혀 엉뚱한 값을 불러오게되는 현상이 일어날 수 있습니다. 이런 문제를 막기 위해 스코프를 분리시키면 데이터 사이의 충돌을 방지할 수 있고, 코드 숨기기는 이를 위한 유일한 도구입니다.

 

덧붙이자면 최근에는 충돌 방지에 의존성 관리자를 활용한 모듈을 사용합니다. Node.js에서도 package.json이라는 이름으로 프로젝트에 포함되는 모듈들을 node_modules 디렉토리에 추가하여 따로 관리하는 것을 보셨을겁니다. 이러한 방법을 통해 굳이 글로벌 스코프에 확인자를 추가 할 필요없이 사용할 수 있습니다. 물론 모듈을 사용해서 얻는 효과는 프로그래머의 편의성에 관련된 것이고, 실제로 렉시컬 스코프로부터 자유로워지는 것은 아니라는 점은 기억하고 넘어가시면 좋겠습니다.

 

 

스코프를 오염시키지 않는 함수

바로 사용해야 하는 함수를 아래처럼 선언하는 것은 비효율적입니다.

 

function abc() {
    let abc = "abcde";
    console.log(abc); // abcde
}

abc();

 

console.log(abc) 를 실행하기 위해 이런 모양의 함수를 사용하게되면 글로벌 스코프에 abc()라는 (한 번만 쓰고 필요 없어지는) 함수가 생성되고, 추가적으로 abc() 를 통해 호출해줘야합니다.

 

이럴때는 괄호를 통해 즉시 실행 함수로 만들어주는 것이 효율적입니다.

 

(function abc() {
    let abc = "abcde";
    console.log(abc); // abcde
})();

 

소괄호로 함수 전체를 감싸주는 것만으로 즉시 실행 함수가 되었습니다. 글로벌 스코프에도 아무런 영향을 미치지 않고, 거추장스러웠던 함수 호출문도 () 하나에 사라진 모습입니다. 이런 형식을 함수 선언문이 아닌 함수 표현식이라고 합니다.

 

이런 함수 표현식을 어디선가 본 듯한 느낌이 드신다면 아마 아래와 같은 익명 함수가 아닐까 추측해봅니다.

 

setTimeout((function() {
    let abc = "abcde";
    console.log(abc); // abcde
}), 1000);

 

setTimeout과 같이 동작을 인자로 받는 함수에서는 위와 같은 익명 함수 표현식을 많이 사용한 경험이 있으실겁니다. 일일히 함수를 만들고 호출할 필요없이 함수를 인라인 시켜 사용하는 방법을 사용하면 간단하고, 금방 원하는 결과를 얻을 수 있습니다.

 

물론 이러한 방법을 사용하면 디버깅하기 어려워지고, 익명 함수의 대표적인 특징인 스스로를 호출할 수 없(진 않지만 좋지 않은)다는 단점이 있지만 인라인해서 사용할만큼 간단한 함수에게까지 굳이 이름을 붙혀줄 필요는 없으니 넘어가도록 하겠습니다.

 

 

블록 스코프

맨 처음 이야기 한 것처럼, 함수가 아니어도 스코프를 생성할 수 있습니다. 그 예시가 바로 블록 스코프입니다.

 

함수를 이용한 스코프가 가장 흔하고 일반적인 방식이긴 하지만, 블록을 이용한 스코프도 상황에 따라 적절히 사용하면 깔끔한 코드를 작성하는데 도움이 됩니다. 요점은, 변수를 실제 사용하는 곳에 가깝게 배치하여 최대한 작은 유효 범위를 갖도록 하는것이 가장 큰 목적이라고 할 수 있겠습니다.

 

블록 스코프를 구성하는 방법은 크게 세 가지가 있습니다.

 

첫 번째는 with 키워드를 사용하는 것입니다. with 키워드 자체는 최대한 지양하는 것이 바람직하다고 했습니다만, 블록 스코프의 구조를 나타내는 대표적인 예시이기도 합니다. with 문 안에서 생성된 객체는 바깥 스코프에 영향을 끼치지 않고, 문 안에서만 존재하다가 문이 끝남과 동시에 사라집니다. (LHS로 인해 글로벌 스코프에 변수가 생성되는 것은 예외적인 경우입니다)

 

두 번째는 try/catch 구문을 사용하는 것입니다. try/catch 구문 중 catch 문에서 선언된 변수는 catch 블록 스코프에 속한다는 특징을 갖고 있습니다.

...
catch(e) {
    console.log(e);
}

console.log(e); // Reference Error!

 

예외나 오류를 담을때 사용한 인자를 catch 문 이후에 사용할 수 없는 것이 그 예시입니다. 에러를 글로벌 스코프까지 끌고 나올 일이 많이 없기 때문에 잘 모르는 사실이지만, 엄연한 스코프를 생성하고 있다는 것을 알 수 있습니다.

 

 

가장 중요하다고 생각되는 세 번째 경우는 let/const 선언을 사용하는 것입니다. ES6에서 추가된 변수 선언 키워드인 let과 const는 블록으로 감싸주었을 경우 그 스코프에서만 존재하는, 블록 자체를 스코프로 받아들이게 됩니다.

 

{
    var a = 123;
    let b = 123;
    const c = 123;
}

console.log(a); // 123
console.log(b); // Reference Error!
console.log(c); // Reference Error!

 

이렇게 명시적으로 블록을 만들어 변수를 선언해주면 나중에 혼동할 여지도 적어지게 되고, 또한 변수의 영역을 한정지어 효율적인 코딩을 할 수 있게 됩니다.

 

 

 

여기까지 혼란스러웠던 스코프의 개념에 대해 알아보았습니다. 처음 접했을땐 어렵지만 효과적인 코딩을 위해서는 꼭 알아야 할 개념이니 여러번 반복하여 숙달되면 좋을 것 같습니다.

 

 

반응형

댓글