Javascript

[Javascript] 함수 선언(Function declaration)과 함수 표현(Function expression)의 차이를 이해하자 - 함수 호이스팅

apost 2023. 1. 30. 02:50

자바스크립트는 함수를 선언하는 방식이 두 가지 있습니다.

함수 선언(Function Declaration)과 함수 표현(Function Expression)으로 함수 정의 방법을 구분하고, 둘 의 가장 큰 차이는 호이스팅(Hoisting)이 지원되는지 여부로 구분을 할 수 있습니다.

 

호이스팅은 순서대로 실행이 되는 인터프리터 방식 언어인 자바스크립트에서 코드 아래쪽에 정의된 함수를 코드 위쪽에서 사용할 때 정의되지 않은 함수 에러를 막아주는 역할을 합니다. 코드 아래쪽에 정의된 함수 이름을 코드 위쪽에 선언해 줘서  함수 실행 위치에서 함수가 정의되었다고 인식할 수 있도록 해줍니다.

 

 

1. 함수 선언과 함수 표현의 차이

 

먼저 기본적인 함수 선언 방식으로 호이스팅 지원을 확인하겠습니다.

함수 선언 코드 앞쪽에서 함수를 호출하지만 에러 없이 잘 실행됩니다.

 

decfunc();
function decfunc(){
    return 'decfunc run!';
}

 

호이스팅이 지원되지 않는 함수 표현으로 같은 함수 구현 코드를 작성합니다. 함수 본체 앞에서 함수를 호출하면 초기화가 되지 않았다는 에러가 발생합니다.

 

expfunc(); // Uncaught ReferenceError: Cannot access 'expfunc' before initialization
let expfunc = function(){
    return 'expfunc run!';
}

 

 

함수 표현의 다른 방식인 화살표 함수도 함수 본체 앞에서 함수를 호출하면 에러가 발생합니다.

함수 표현을 호출하는 앞쪽에 변수를 미리 선언하면 어떻게 될까? 마찬가지로 에러가 발생합니다.

에러의 종류는 조금 달라져서 함수가 아니라는(is not a function) 에러가 표시됩니다.

 

변수가 앞서 선언되어 있지만, 함수 표현을 대입해서 변수가 함수를 가리키게 될 때까지 선언된 변수는 데이터를 담는 일반 변수가 됩니다. 따라서 변수를 expfunc()와 같은 함수명으로 호출할 수 없습니다.

 

let expfunc;
expfunc(); // Uncaught TypeError: expfunc is not a function
expfunc = function(){
    return 'expfunc run!';
}

 

 

 

 

2. 함수 표현이 필요할 때가 있다.

 

이왕이면 호이스팅도 지원되는 함수 선언을 쓰면 되겠지만 필요도 없는 함수 표현을 만들었을 리는 없고, 실제로 함수 표현이 모던 자바스크립트 환경에서는 더 많이 사용되고 있습니다.

 

그리고, 함수 선언으로 많은 함수들이 호이스팅 되다 보면 함수의 스코프 이슈가 발생해서 선언 중복이나 원하지 않는 함수가 실행되는 문제가 발생합니다. 편하지만 그만큼의 잠재적인 오류 발생 가능성을 내포하고 있습니다.

 

함수 표현을 사용하는 가장 중요한 이유는 함수 표현이 클로저를 지원하기 때문입니다.

이벤트 핸들링을 할 때 특히 중요한 특징으로 핸들러 함수를 실행하는 시점의 함수를 호출하는 구문 환경 정보(Lexical Environment)를 기억하는 것을 클로저라고 합니다.

예를 들어 정의된 함수 표현을 호출하는 시점에 호출 위치의 스코프 안에 있는 변수와 객체 정보 등이 구문 환경 정보가 됩니다.

 

함수 표현이 클로저를 지원함으로써 최적화 된 간략한 코드를 작성할 수 있고, 함수를 실행하는 시점의 실행 환경 정보를 저장할 

 

함수 표현을 사용하는 대표적인 경우를 예를 들어보겠습니다.

다음처럼 세 개의 버튼으로 구성된 UI에서 버튼을 클릭하면 각각의 이벤트가 발생하도록 루프문으로 이벤트를 등록해 보겠습니다.

 

<input type="button" name="button1" id="button1" class="button" value="버튼1">
<input type="button" name="button2" id="button2" class="button" value="버튼2">
<input type="button" name="button3" id="button3" class="button" value="버튼3">

 

 

먼저 호이스팅을 지원하는 함수 선언으로 다음 루프문을 실행해 보겠습니다.

!중요합니다.

루프문 루프를 위해 인덱스로 선언하는 변수 i는 루프문 외곽에 별도로 선언해야 합니다. 개념을 혼동할 수 있기 때문에 루프문 외곽에 선언하는 방식으로 설명을 합니다. 이유는 뒤에서 따로 설명합니다.

 

    var buttons = document.querySelectorAll('.button');

    let i;
    for (i = 0; i < buttons.length; i++) {
        buttons[i].onclick = (e)=>{console.log("index: "+i)}; //3
    }

 

 

버튼을 누르면 콘솔창에 3이 출력됩니다.

클릭 이벤트 핸들러 함수로 함수 표현을 대입했지만 루프를 돌던 시점의 해당 인덱스 값이 아닌, 루프문을 완전히 빠져나왔을 때의 변수  i의 값인 3이 반복해서 출력됩니다.

변수 i는 전역 변수이기 때문에 루프문 스코프에 전혀 영향을 받지 않고 클릭 이벤트가 발생한 시점에 전역 변수 i의 값을 참조합니다.

 

앞서의  루프문을 클로저를 지원하는 함수 표현으로 구현해서 인자로 넘긴 루프문 인덱스를 출력할 수 있도록 해보겠습니다. clickHandler()는 함수 표현이므로 루프문 상단에 선언해야 에러가 발생하지 않습니다.

 

var buttons = document.querySelectorAll('.button');

let clickHandler = (index)=>{
    return ()=>{
        console.log("index: "+index); // index를 함수 인자로 받지 않았지만 핸들러로 받은 루프 인덱스 값에 접근 가능
    }
}

let i;
for (i = 0; i < buttons.length; i++) {
    buttons[i].onclick = clickHandler(i);// clickHandler()를 호출할 때 넘긴 인덱스의 값을 기억함
}

 

버튼을 클릭하면 다음처럼 루프문을 돌 때 대입한 루프문의 변수 i 값이 각각 출력됩니다.

앞서 설명한 대로 함수 표현의 클로저 특성에 의한 것입니다.

중요한 것은 clickHandler() 함수가 아니라 clickHandler() 함수가 return으로 반환하는 함수 표현입니다. 함수 표현은 실행 시점의 구분 환경 정보를 별도의 저장 공간에 저장해서 기억을 하고 있기 때문에 반환하는 함수 코드는 루프문이 돌던 시점의 인덱스 값을 기억하고 있게 됩니다.

그래서, 클릭 이벤트가 발생하는 시점에 실행되는 함수 코드는 기억하고 있던 index 값을 사용하게 됩니다.

 

 

 

 

3. 스코프와 클로저

앞서 버튼 클릭 시 3만 출력하던 코드의 루프문을 다음과 같이 수정해 보겠습니다.

전역 변수였던 i를 루프문 지역 변수로 for 안에 선언을 해서 변수 i의 스코프를 루프문 안으로 제한합니다. 전역 변수로 선언한 i는 삭제합니다.

그리고 기적처럼? 루프문을 돌던 시점의 인덱스 값이 출력됩니다.

 

클릭 이벤트 핸들러에 대입한 함수는 함수 표현으로 선언됩니다. 실행 시점의 구문 환경 정보를 저장하므로 루프를 돌던 시점의 변수 i의 값을 기억합니다. 그러니까 앞의 코드는 함수 표현이 잘못된 게 아니라 전역으로 선언된 변수 i로 인해 기억해 놓은 구문 환경 정보의 i 값을 가져오지 못한 것입니다.

 

for (let i = 0; i < buttons.length; i += 1) {
    buttons[i].onclick = (e)=>{console.log("index: "+i)}
}

 

코드를 다시 조금 바꿔서 다음과 같이 변수 i를 전역으로 추가 선언합니다.

자바스크립트에서는 이렇게 같은 변수명으로 중복 선언을 해서 사용할 수 있고, 각각의 스코프 범위 안에서 개별 동작을 합니다.

 

let i=100;
for (let i = 0; i < buttons.length; i += 1) {
    buttons[i].onclick = (e)=>{console.log("index: "+i)}
}
console.log(i)

 

 

실행 결과를 보면 알겠지만, 이벤트 핸들러로 추가한 함수 표현은 이벤트가 발생할 때까지는 콘솔에 출력하는 내용이 없습니다. 따라서 전역으로 선언한 i인 100이 가장 먼저 출력됩니다.

그리고 버튼을 클릭하면 클로저에 의해 함수 표현이 기억하고 있던 루프문이 돌던 시점의 로컬 변수 i의 값이 출력됩니다.

함수 표현을 사용할 때 자주 실수하는 부분이 변수의 스코프를 착각해서 정확히 원하는 값이 전달되지 않는 것입니다.

 

 

 

 

3. 함수 표현과 함수 선언의 착각

 

다음 함수는 함수 선언일까요? 아니면 함수 표현일까요?

 

let exnamefunc = function namefunc(){
    console.log('namefunc run!')
}

 

함수를 실행해 보면 바로 알게 됩니다.

 

expnamefunc() // namefunc run!
namefunc() // Uncaught ReferenceError: namefunc is not defined

 

네 함수 표현입니다.

함수 선언 몸체에 함수 이름이 있어도 변수에 대입을 하면 함수 표현으로 선언됩니다. 즉, "namefunc()"은 함수가 아니기 때문에 사용할 수 없고, 함수 표현 변수명으로 호출해야 함수가 실행됩니다.

 

앞서 함수 표현으로 만들었던 클릭 핸들러를 다음처럼 표현할 수 도 있습니다.

 

let clickHandler = (index)=>{
    return function callBackFunc(){
        console.log("index: "+index)
    }
}

 

return으로 반환하는 callBackFunc() 함수는 언뜻 함수 선언처럼 보이지만, 함수 표현이고, 클로저를 지원하므로 상위 함수(clickHandler())의 index 값을 기억하고 있다가, 클릭 이벤트가 발생하면 해당 값을 출력하게 됩니다.

clickHandler()가 return 하는 함수 코드는 결국 clickHandler에 구문 환경 정보와 함께 대입이 되기 때문에 함수 표현으로 정의됩니다.

함수 작성 구문 패턴이 아니라 최종적으로 변수에 대입이 되는지가 함수 표현인지를 결정하게 됩니다.