JavaScript: 어떤 방식으로 함수 정의하는 것을 선호하세요? - (1)

함수 선언문(스코프, 호이스팅)에 대해

·

5 min read

들어가며

인터뷰에서 "어떤 방식으로 함수 정의하는 것을 선호하세요?" 라는 질문을 받은 적이 있다. 그 당시에는 JS에 관한 공부가 부족했기에 정의 방식에 따라 어떤 차이가 있는지 확실하게 알지 못했다. 고로 대답은 했지만 그에 대한 근거가 부족했다. JS를 공부하고 나서 이 질문의 큰 그림을 알게 되었다. 함수 정의 방식에 따라 스코프, 호이스팅, this 등 JS의 다른 개념들이 숨어있었고 그 개념들까지 제대로 알고 있느냐를 판단하기 위한 질문이었다. 그럼 저 질문에 대비하기 위해 이 글에서 함수 정의 방식에 대해 자세히 알아보자.

함수 선언문 - Function Declaration

greeting();

function greeting() {
    console.log("Hello world!");
}
// Hello world!

위와 같은 방식으로 함수를 선언하는 방식을 우리는 함수 선언문이라고 한다. 근데 위의 코드를 자세히 보면 함수를 선언하기 전에 호출했다. 오류가 난다고 생각할 수 있지만 위 코드는 정상적으로 작동한다.

인터프리터 언어 vs 컴파일 언어

JS 인터프리터는 성능 향상을 위해 JIT 컴파일(just-in-time compile)이라는 기술을 사용한다. JIT 컴파일이란 스크립트의 실행하는 시점에 기계어로 번역하는 컴파일 기법이다. 그렇다면 왜 컴파일 언어가 아니라 인터프리터 언어로 분류되는 것일까? 컴파일이 미리 처리되는 것이 아니라 런타임에 처리되기 때문이다. 보통의 인터프리터 언어에서 위의 코드를 실행하면 오류가 나겠지만 JS는 JIT 컴파일 기술을 사용하고 있기 때문에 위의 코드가 정상 작동하는 것이다.

그리고 위의 코드에서 나타나는 현상과 같은 특징을 '호이스팅' 이라고 한다. MDN 웹 문서 정의에 의하면 호이스팅은 [인터프리터가 코드를 실행하기 전에 함수, 변수, 클래스 또는 임포트(import)의 선언문을 해당 스코프의 맨 위로 끌어올리는 것처럼 보이는 현상]이다.

스코프

여기서 스코프란 [변수가 어떤 것을 참조하는지를 결정하는 규칙 모음]이다. YDKJY 저자는 스코프를 쉽게 설명하기 위해 양동이, 변수는 양동이에 넣을 구슬에 비유한다. 예시를 보자.

전체를 검정색 양동이, 함수 선언문을 빨간색 양동이, for문을 노란색 양동이라고 하자. 빨간색 양동이는 노란색 양동이를 감싸고 있고 검은색 양동이는 빨간색 양동이를 감싸고 있다. 이렇게 스코프는 필요에 따라 원하는 만큼 중첩해서 사용할 수 있고 스코프와 중첩 스코프 사이에 맺어진 연결을 스코프 체인이라고 한다.

이렇게 중첩 된 스코프의 경우 하위 스코프에서는 상위 스코프에 있는 변수나 식별자를 참조할 수 있지만, 상위 스코프에서는 하위 스코프 참조가 불가능하다. 노란색 양동이에서는 빨간색/검정색의 구슬에, 빨간색 양동이에서는 검정색 구슬에 접근할 수 있지만, 검정색 양동이에 있는 표현식은 빨간색이나 노란색 구슬에 접근 할 수 없다는 것이다.

위의 코드에서 양동이와 구슬을 찾아보자. 검정색 양동이 안에는 students(첫 번째 줄), getStudents(일곱 번째 줄) 구슬이 있고, 빨간색 양동이 안에는 studentId 구슬이 있다. 마지막으로 노란색 양동이에는 for 반복문에 있는 student(여덟 번째 줄) 구슬이 있다. 이렇게 구슬(변수)은 특정 양동이(스코프)에 담을 수 있다. 구슬은 특정 양동이에 담겨 있다. 이 말은 즉, 변수는 특정 스코프에서 선언된다는 것이다.

검정색 양동이에 담긴 구슬처럼 어느 위치에서든 참조가능 한 변수를 전역변수라고 하고 빨간색 양동이와 노란색 양동이에 담긴 구슬처럼 특정 구역({}) 내에서 선언 된 변수를 지역변수라고 한다.

for문에 있는 students 는 무엇을 참조할까? 먼저 빨간색 양동이 내에서 students 라는 이름을 가진 구슬을 찾게 되는데 조건에 맞는 구슬이 없다. 그러면 상위 양동이인 검정색 양동이에서 원하는 구슬이 있는지 찾게 될 것이고 검정색 양동이에는 이름이 students 인 구슬을 찾았으므로 검정색 양동이에 있는 구슬을 참조하게 된다. 이렇게 스코프 체인을 따라 상위 스코프로 올라가면서 참조 할 구슬을 탐색한다.

그럼 이런 양동이의 범위는 어떻게 정해지는 것일까? ES2015(ES6)이 공개 되기 전에는 스코프를 함수 레벨로 제한했다.

function greeting(name, type) {
    if (type === 1) {
        var message = '안녕하세요.';
    } else if (type === 2) {
        var message = '안녕히 가세요.';
    }
    console.log(`${name}님, ${message}`);
}

greeting('지은', 1);
// 지은님, 안녕하세요.

함수 레벨 스코프

var 키워드로 정의한 변수는 함수 레벨 스코프를 사용한다. 함수 내에서 선언한 변수는 함수 내에서만 참조할 수 있으며 함수 외에서는 참조할 수 없다. 위의 코드를 보면 if 블록 내에서 message 변수를 정의했지만 if 블록을 벗어나도 message를 참조할 수 있다.

블록 레벨 스코프

ES2015 공개 이전에는 함수 레벨 스코프만 존재했지만 ES2015에서 변수를 정의하는 또 다른 키워드인 letconst 가 도입되면서 이들의 참조 범위를 블록 레벨 스코프로 제한했다. 블록 레벨 스코프란 변수 참조 범위를 블록({}) 으로 제한하는 것이다.

function greeting(name, type) {
    let message = '';
    if (type === 1) {
        let message = '안녕하세요.';
    } else if (type === 2) {
        let message = '안녕히 가세요.';
    }
    console.log(`${name}님, ${message}`);
}

greeting('지은', 1);

그렇다면 위의 코드를 실행하면 어떻게 출력될까? let 키워드는 블록 레벨 스코프를 따르기 때문에 if 문 안에 정의 된 message 들은 if 문 안에서만 참조 가능하다. 그렇기에 여덟 번째 줄에서 참조하는 message는 함수 상단에 정의 된 빈 문자열이 될 것이고 최종적으로 지은님, 이 출력된다.

호이스팅

그럼 여기서 다시 함수 선언문의 코드를 가져와 호이스팅에 대해 알아보자. 호이스팅의 정의를 다시 한 번 언급하자면 [인터프리터가 코드를 실행하기 전에 함수, 변수, 클래스 또는 임포트(import)의 선언문을 해당 스코프의 맨 위로 끌어올리는 것처럼 보이는 현상]이다.

호이스팅이 발생하는 원리

JS 엔진은 JS 파일을 실행하기 전에 컴파일 과정을 먼저 거친다고 앞에서 설명했다. 함수 또는 변수를 스코프에 등록하는 과정을 [선언(식별자 정의), 초기화(undefined 할당), 할당(값 등록)] 세가지 단계로 나눠보자. 컴파일 과정에서 JS가 코드를 읽다 함수/변수를 만나게 되면 세 단계 중 선언 단계는 필수로 이뤄진다. 해당 식별자(구슬)를 스코프(양동이)에 등록하는 것이다. (초기화와 할당은 함수냐 변수냐에 따라, 변수를 어떤 키워드로 정의하냐에 따라 달라진다. 이 부분은 후술 예정이다.) 컴파일이 끝나고 런타임이 시작되면 JS 엔진은 다시 순서대로 코드를 읽는다. 코드를 읽다가 함수/변수를 만나게 되면 스코프에 함수/변수가 등록되어 있는지 확인 후 해당 코드를 처리하는데, 함수/변수 등록이 어느 단계까지 실행됐냐에 따라 JS 엔진의 처리 방법은 달라진다.

함수 호이스팅

greeting();

function greeting() {
    console.log("Hello world!");
}

// Hello world!

위의 코드를 실행하면 greeting 이라는 함수를 선언하기 전에 함수를 참조할 수 있다. 함수 선언문으로 함수를 정의하면 컴파일 과정에서 [선언(식별자 정의), 초기화(undefined 할당), 할당(값 등록)] 세 단계를 모두 거치게 되고, 이를 함수 호이스팅이라고 한다. 그러므로 함수 선언문으로 정의한 함수는 스코프 내 어디에서든 호출할 수 있다.

변수 호이스팅

호이스팅의 정의를 보면 '변수' 또한 호이스팅 된다고 설명되어 있다. 그럼 아래의 코드를 실행하면 어떻게 될까?

console.log(name);
var name = '지은';

앞서 설명한 함수 호이스팅을 생각하면 지은이 출력 될 것이라 예상된다. 하지만 결과를 보니 undefined이 출력된다. 왜 그런 것일까? 변수의 경우 함수 호이스팅과는 다르게 컴파일 시점에서 선언과 초기화 단계만 이뤄지기 때문에 undefined 가 출력되는 것이다.

name = '승철';
console.log(name);
var name = '지은';
// 승철

위의 코드에서 선언하기 전에 첫 번째 줄에서 name에 값을 할당 할 수 있다. 이는 식별자가 호이스팅 되고 스코프의 최상단에서 undefined로 자동 초기화 되기 때문에 가능한 것이다.

그럼 변수를 var 키워드가 아닌 let 키워드로 정의하면 어떻게 될까?

console.log(name);
let name = '지은';

위의 과정을 잘 따라왔다면 undefined가 출력 된다고 생각 할 것이다. 틀렸다. 아래와 같이 에러가 난다.

var 키워드로 선언한 변수의 호이스팅과 let/const 로 선언한 변수의 호이스팅은 차이가 있다. var 키워드의 경우 코드가 실행되기 전 선언, 초기화 단계를 거치지만 let/const 로 선언한 변수의 경우 선언 단계만 거치게 된다.

여기서 스코프에 진입 한 후, 변수의 초기화가 일어나기까지의 시간을 TDZ(temporal dead zone)라고 한다. TDZ란 [변수는 존재하지만 초기화되지 않아 어떤 방식으로도 해당 변수에 접근할 수 없는 시간대]이다. let/const의 TDZ는 컴파일에서 변수가 선언된 후 런타임에서 변수가 초기화 될 때까지의 시간이라고 할 수 있다.

참고

[책] You Don't Know JS, Yet - 카일 심슨 저/이보라| 한빛미디어
https://developer.mozilla.org/ko/docs/Learn/JavaScript/First_steps/What_is_JavaScript
https://developer.mozilla.org/ko/docs/Glossary/Hoisting