본문 바로가기
JAVASCRIPT

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

by KBS 2022. 2. 16.
728x90

스코프 역할을 하는 함수

지금가지 코드를 함수로 감싸 내부에 변수나 함수 선언문을 바깥 스코프로부터 함수의 스코프 안에 '숨기는' 것을 살펴보았다.

var a = 2;
function foo() {
  var a = 3;
  console.log(a); // 3
}

foo();

console.log(a); // 2

 

이 방식은 작동하기는 하지만, 결코 이상적인 방식은 아니다. 이 방식에는 몇가지 문제가 있다. 첫째, foo()라는 이름의 함수르 선언해야 한다. 즉, foo라는 확인자 이름으로 둘러싸인 스코프를 '오염시킨다'는 의미다. 또한, 그 함수를 직접 이름으로 호출해야만 실제 감싼 코드를 실행할 수 있다.

함수를 이름없이 선언하고 자동으로 실행된다면 더 이상적일 것이다.

다행이도 자바스크립트에서는 두 가지 문제를 모두 해결할 방법이 있다.

var a = 2;
(function foo() {
  var a = 3;
  console.log(a); // 3
})();

console.log(a); // 2

 

이 코드가 어떤 식으로 작동하는지 하나하나 살펴보자.

먼저, 코드를 감싼 함수는 function ...이 아니라 (function... 으로 시작한다. 별 다를바 없어 보일 수도 있지만 실제로는 큰 변화를 가져온다. 이 코드에서 함수는 보통의 선언문이 아니라 함수 표현식으로 취급한다.

선언문과 표현식을 구분하는 가장 쉬운 방법은 'function'이라는 단어가 구문에서 어디에 위치하는가를 살펴보면 된다. 'function'이 구문의 시작 위치에 있다면 함수 선언문이고, 다른 경우는 함수 표현식이다.

여기서 볼 수 있는 함수 선언ㅇ문과 함수 표현식의 중요한 차이는 함수 이름이 어디의 확인자로 묶이느냐와 관련이 있다.

앞의 두 코드를 비교해보자. 첫째 코드에서 함수 이름 foo는 함수를 둘러싼 스코프에 묶이고, foo()라는 이름을 통해 직접 호출했다. 두 번재 코드에서 함수 이름 foo는 함수를 둘러싼 스코프에 묶이는 대신 함수 자신의 내부 스코프에 묶였다. 즉, (function foo(){...})라는 표현식에서 확인자 foo는 오직 '...'가 가리키는 스코프에서만 찾을 수 있고 바깥 스코프에서는 발견되지 않는다. 함수 이름 foo를 가지 내부에 숨기면 함수를 둘러싼 스코프를 불필요하게 오염시키지 않는다.

익명 vs 가명

다음과 같이 함수 표현식을 콜백 인자로 사용하는 사레에 익숙할 것이다.

setTimeout(function () {
  console.log("I waited 1 second!");
}, 1000);

 

이런 방식을 '익명 함수 표현식'이라 부르는데 이는 function() ...에 확인자 이름이 ㅇ벗기 때문이다. 함수 표현식은 이름 없이 사용할 수 있지만, 함수 선언문에는 이름이 빠져서는 안된다. 이름 없는 함수 선언문은 자바스크립트 문법에 맞지 않는다.

익명 함수 표현식은 빠르고 쉽게 입력할 수 있어서 많은 라이브러리와 도구가 이 자바스크립트 특유의 표현법을 권장한다. 그러나 함수 표현식은 몇 가지 기억할 단점이 있다.

  1. 익명 함수는 스택 추적시 표히살 이름이 없어서 디버깅이 더 어려울 수 있다.
  2. 이름 없이 함수 스스로 재귀 호출을 하려면 붕행히도 폐기 예정인 argument.callee 참조가 필요하다. 자기 참조가 필요한 또다른 예로는 한 번 실행하면 해제되는 이벤트 처리 함수가 있다.
  3. 이름은 보통 쉽게 이해하고 읽을 수 있는 코드 작성에 도움이 되는데, 익명 함수는 이런 이름을 생략한다. 기능을 잘 나타내는 이름은 해당 ㅋ ㅗ드를 그 자체로 설명하는 데 도움이 된다.

인라인 함수 표현식은 매우 효과적이고 유용하다. 익명이냐 가명이냐의 문제가 이사실을 퇴색시키지는 않는다. 함수 표현식에 이름을 사용하면 특별한 부작용 없이 상당히 효과적으로 앞의 단점을 해결할 수 있다. 따라서 함수 표현시글 사용할 대 이름을 항상 쓰는 것이 좋다.

setTimeout(function timeoutHandler() {
  console.log("I waited 1 second!");
}, 10000);

함수 표현식 즉시 호출하기

var a = 2;

(function foo() {
  var a = 3;
  console.log(a); // 3
});

console.log(a); // 2

 

()로 함수를 감싸면 함수를 표현식으로 바꾸는데, (function foo(){})() 처럼 마지막에 또 다른 ()를 붙이면 함수를 실행할 수 있다. 함수를 둘러싼 첫 번째 ()는 함수 표현식으로 바꾸고, 두번째 ()는 함수를 실행시킨다.

이런 패턴은 굉장히 흔해서 개발자 커뮤니티에서는 몇 년 전 이것을 부르는 즉시 호출 함수 표현식이라는 용어를 정하기도 했다. 이 말은 즉시, 호출, 함수, 표현식의 합성어다.

물론, IIFE는 이름이 꼭 필요하지는 않다. IIFE는 익명 함수 표현식으로 가장 흔하게 사용된다. 분명히 덜 흔하기는 하지만, IIFE를 기명으로 사용하면 익명 함수 표현식을 사용하는 것보다 앞서 언급한 것처럼 더 나은 면이 있다. 따라서 기명 IIFE를 사용하는 것은 좋은 습관이다.

var a = 2;
(function IIFE() {
  var a = 3;
  console.log(a); // 3
})();

console.log(a); // 2

 

어떤 이들은 전통적인 IIFE 형태를 약간 변형하여 (function foo(){})()로 사용하기도 한다. 자세히 살펴보면 차이가 보일 것이다. 첫재 형태에서 함수 표현식은 ()안에 싸여있고, 호출에 사용되는 ()가 밖에 바로 붙어 있다. 둘째 형태에서 호출에 사용되는 ()는 둘러싼 ()안으로 옮겨진다. 두 형태 모두 똑같이 기능한다. 순전히 어떤 스타일을 선호하느냐의 문제일뿐이다. 널리 사용되는 또 다른 IIFE의 변형은 IIFE가 결국은 함수라는 사실을 이용해 인자를 넘기는 방식이다.

var a = 2;
(function IIFE(global) {
  var a = 3;
  console.log(a); // 3
  console.log(global.a); // 2
})(window);

console.log(a); // 2

 

예제에서는 window객체 참조를 global이라 이름 붙인 인자에 넘겨서 글로벌 참조와 비 글로벌 참조 사이에 명확한 차이를 만들었다. 물론, 해당 스코프에 무엇이든 넘길 수 있고 인자 이름도 마음대로 지을 수 있다. 이는 어디까지나 스타일 선호의 차이일 뿐이다.

이 패턴의 다른 예제를 통해 기본 확인자 undefined의 값이 잘못 겹쳐 쓰여 예상치 못한 결과를 야기하는 문제에 대해 살펴보자. 인자를 undefined라고 이름 짓고 인자로 아무 값도 넘기지 않으면, undefined 확인자의 값은 코드 블록 안에서 undefined를 가진다.

 


참고

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

댓글