2016년 1월 1일 금요일

javascript: Function Declaration and Expression ; 함수의 선언 방식

Javascript: 함수의 선언 방식

이번엔 Javascript에서 함수를 표현하고 정의하는 몇가지 방식과 각 방식들의 차이점, 특징들을 살펴보겠습니다.

함수 생성 방법

보통 Javascript에는 아래와 같이 세가지 함수 생성방법이 존재합니다.

Function Declaration( 이하 ‘FD’로 표현 합니다. )
함수 선언, 정해진 함수 선언문 형식으로 함수를 정의하며, C나 Java와 같은 언어에서 말하는 ‘선언’과 ‘정의’를 따로 구분하지 않는다. 함수의 선언문에 정의부가 항상 포함되어야 하는 구조로 ECMAScript1에서 정의하고 있다. 일반적인 특징은 정의부 마지막에 세미콜론(;)이 없으며 아래와 같이 정의된다( 마지막에 세미콜론이 있더라도 Browser가 이를 무시하는 경우가 대부분임 ).
function “함수명” ( [“매개변수 리스트 “] ) {
“함수 정의부”
}
Function Expression( 이하 ‘FE’로 표현 합니다. )
일반적으로 대입연산자(=)의 우변에 올 수 있는 표현식형태이거나 실행시간(run-time)에 동적으로 실행해야 하는 표현식 형태로 정의된다. 이름이 없는 형태로 많이 쓰이고, 구문분석시 함수의 선언문이 “표현식”으로 인지되면 “함수 선언”과는 다른 형태로 정의가 된다. 런타임에 실행되는 모든 표현식의 마지막이 세미콜론(;)으로 끝나듯, 함수 표현식도 세미콜론으로 정의부가 끝난다( Self-invoking function의 경우는 약간 예외가 될 수 있습니다. 이는 이후 내용에 설명하겠습니다 ).
var func = function [ “함수명” ] ( [“매개변수 리스트”] ) {
“함수 정의부”
};
Function creation with constructor( 이 방법은 그냥 넘어가겠습니다. )
new Function() 이라는 함수 생성자를 사용하여 생성하는 방식으로, 문자열로 복잡한 함수정의부를 구현해야 하는 단점으로 인해 현실적으로 잘 사용되는 방법은 아니다. 형태는 다음과 같다.
var func = new Function( “p1”, “p2”, “return p1 * p2 ” );

함수 생성방식별 특징

이 글을 쓰고 있는 2015년 12월 시점의 ECMAScript 의 최신 버전은 ECMA-262 6개정판( 2015년 6월 제정 )이지만 아마도 이를 100% 구현하고 있는 브라우저는 아직 없는것 같습니다. 아마 ECMA 5.x 버전을 대부분 지원할텐데요, ECMAScript 표준 자체에 브라우저별 세부 구현을 모두 정의하고 있는것이 아니라서 브라우저별로 약간씩 다른 결과를 보여줄 수는 있습니다. 이에 대해서는 제가 모두다 테스트를 해보지 못했기에, Google Chrome 47.0.x.x 버전을 기준으로 설명드리도록 하겠습니다. ECMAScript 기준으로 하면 5.1 버전 기준이 되겠네요.

함수 선언( Function Declaration )

앞선 포스트들( javascript: Hoisting / 호이스팅, javascript: Scope of variables and functions ; 변수와 함수의 범위 )에서 FD방식으로 정의된 함수들이 해당 Scope의 가장 처음으로 끌어올려진다는 사실을 아셨을 것입니다. 또한, 함수 선언문은 정의부에 또다른 함수 선언문을 가질 수 있다는 것도 아셨을 것입니다. 사실 이 것들이 가장 큰 특징들이며 function 키워드 앞에 공백문자를 제외하고 아무것도 오지 않아야 합니다. 이러한 호이스팅 특성 때문에 조건에 따라 함수를 정의하기가 어렵고 의도하지 않은 오버라이딩이 발생하기도 하지만, 어디서든 정의를 하면 어디서든 사용할 수 있다는 장점이 있습니다.

함수 표현( Function Expression )

함수 표현방식은 주로 변수에 할당하는 경우가 많은데 그러다보니 대입연산자( = ) 우측에 함수 정의부가 오는 경우가 많습니다. 대입연산자 없이 Javascript인터프리터가 동적으로 해석해야 하는 일반적인 표현식으로 되어 있는 경우에도 함수 표현식으로서 해석이 됩니다. “함수 선언”으로 만들어진 함수가 해당 Scope의 가장 처음으로 Hoisting되는것과 대조적으로 일반 표현식이기 때문에 함수 정의부가 별도로 Hoisting 되지는 않습니다( 다만 해당 함수를 담을 변수자체는 해당 Scope 최 상단으로 끌어올려지게 됩니다). 이 특징은 조건에 따른 함수 정의를 가능하게 해줘서 조금더 유연게 설계를 할 수 있다는 장점이 있습니다. 그리고 이후에(언제가 될지는 모르겠지만) 몇몇 Javascript Design Pattern을 적용하기 위해서 필수적으로 필요한 표현식이기도 합니다.

함수 생성방식별 비교

아래의 코드를 먼저 보시죠.

<script type="text/javascript">
    fnc1();
    fnc2();

    var variable = "defined";

    function fnc1() {
        console.log( variable );
    }

    ( function fnc2() {
        console.log( variable );
    } );
</script>

실행결과
undefined
exception[ fnc2 is undefined ]

위의 예를 보시면 “함수 선언”과 선언문을 (, )로 감싸고 있는 두가지 형태의 함수 생성문을 보실 수 있습니다. fnc1는 FD로 정의되어 가장 위로 끌어올려지고, “fnc1();” 구문이 문제없이 실행됩니다. 다만 호이스팅에 의해 의도와는 달리 전역변수에 접근을 못하는 일이 생겼네요. 그리고 fnc2는 FE로 정의된 함수이며, 괄로로 둘어쌓여 있습니다. 이것은 Javascript 표현식이기 때문에 호이스팅 대상이 아닙니다. 그래서 “fnc2();” 표현식이 실행되는 시점에는 그 정의가 없어서 invoke시점에 예외가 발생됩니다. 물론 fnc2를 FE로 정의하고 이를 담는 참조변수가 없기때문에 이 함수를 이후에 다시 호출할 기회가 전혀 없기도 합니다.

아래는 FE방식으로 함수를 생성해서 사용하는 예입니다.

<script type="text/javascript">
    var printUserInput;
    if( prompt( "Enter Y/N" ) == "Y" ) {
        printUserInput = function() {
            console.log( "User said, Yes!" );
        };
    } else {
        printUserInput = function() {
            console.log( "User said, No!" );
        };
    }

    printUserInput();
</script>

위의 코드는 사용자가 정확하게 대문자로 ‘Y’를 입력하면 “User said, Yes!”라는 문자열을 콘솔에 출력하고, 아닌경우는 “User said, No!”라는 문자열을 콘솔에 출력합니다. 사용자가 항상 대문자로 Y를 입력하리란 보장이 없으므로 대소문자의 구분없이 비교하는 함수 하나를 더 만들어 보겠습니다.

<script type="text/javascript">
    // 이 코드는 FE를 예로 들기위해 Java API를 흉내내어 만든 코드입니다.
    // 이해가 잘 안되시는 부분이 있더라도 그냥 넘어가시면 됩니다!
    String.prototype.equalsIgnoreCase = function( operand ) {
        if( this === operand ) {
            return true;
        }

        if( typeof operand === "undefined"
            || typeof operand !== "string" ) {
            return false;
        }

        if( this.toLowerCase() === operand.toLowerCase() ) {
            return true;
        }

        return false;
    };
</script>

<script type="text/javascript">
    var printUserInput;
    if( "Y".equalsIgnoreCase( prompt( "Enter Y/N" ) ) ) {
        printUserInput = function() {
            console.log( "User said, Yes!" );
        };
    } else {
        printUserInput = function() {
            console.log( "User said, No!" );
        };
    }

    printUserInput();
</script>

위의 코드에서 나온 String.prototype….. 부분에 대한 설명은 추후 다른 포스팅에서 설명하기로 하고 그냥 넘어가겠습니다. 어쨌든 해당 코드의 의미는 모든 String 객체에다가 전역적으로 equalsIgnoreCase라는 함수를 정의한 것이고 이때, FE방식으로 해당 함수를 정의했습니다. 한가지 주의해야 할 점은 이 역시 FE이기 때문에 equalsIgnoreCase라는 함수를 사용하기 전에 미리 정의가 되어야 한다는 점입니다( <script> 태그를 별도로 분리한것은 별도의 모듈이란 의미에서 제가 임의로 분리를 한 것이니 크게 신경쓰지 마세요 ).

사실 대소문자를 구분없이 비교하고자 할때, 아래처럼 FD를 사용해서 함수를 만들 수도 있겠죠.

function equalsIgnoreCase( leftOperand, rightOperand ) {
// .... 구현부
}

“이렇게 해서는 안된다.”라는건 아니지만 선택은 여러분의 몫입니다. 이 예제와 같은 케이스에서 저는 개인적으로 FE방식으로 하는게 코드의 양을 줄일수 있다고 보고 있습니다.

이번엔 FD와 FE를 섞은 코드를 한번 보시죠.

<script type="text/javascript">
    getBackToMe( function() {
        console.log( "나 왔어요!" );
    });

    function getBackToMe( callback ) {
        console.log( "돌아오면 연락 주세요." );
        callback();
    }
</script>

아주 기초적인 callback함수 사용예시를 만들어 봤습니다. 분명히 getBackToMe만 호출했는데, 내가 호출한 함수가 매개변수로 넘긴 FE를 대신 실행해 주었지요. 이런 패턴을 callback패턴이라고 하며 FE자체를 callback함수라고 합니다. 위의 예시는 가변적으로 넘어 올 수 있는 FE의 매개변수 등은 고려하지 않은 아주 단순한 예제입니다.

함수 표현식의 종류

지금까지의 예제에서는 FE를 모두 무명함수 즉, Anonymous Function으로 정의를 했습니다. 이름이 없다는 이야기죠. 사실 함수를 참조하는 참조변수의 이름을 함수의 이름으로 볼 수있겠지만, ECMAScript에서 이야기하는 함수 정의의 표준은 서두에서 설명드렸듯이 아래와 같습니다. 함수명과 매개변수 리스트는 생략이 가능하여 [, ]로 감싸져 있죠.

function [ “함수명” ] ( [“매개변수 리스트”] ) {
“함수 정의부”
};

따라서 FE는 아래 두가지로 또다시 구분이 가능해 집니다.

  • Anonymous Function Expression ( AFE ; 무명 함수 표현식 )
  • Named Function Expression ( NFE ; 기명 함수 표현식 )

각각의 예는 다음과 같습니다.

// Anonymous Function Expression( AFE )
var afe = function() {
    console.log( "I do not have name." );
};

// Named Function Expression( NFE )
var nfe = function functionWithName() {
    console.log( "My name is nfe." );
};

이 둘의 기능적인 차이는 단 1%도 없습니다. 다만, 최근 거의 모든 브라우저에 탑재된 Debugger를 사용하면서 Call Stack이나 Profiler 등을 확인할 경우에는 조금 다른 면이 있습니다.

NFE는 정확하게 각 함수의 이름을 표시해 주지만 AFE의 경우 ( anonymous function )과 같은 형태로만 보여줍니다. 기능이 복잡해져서 함수의 개수가 셀 수 없는 지경이고, 코드는 몇천, 몇만 라인이 되는 경우 anonymous function이 대부분이 되게되면 디버깅의 지옥을 맞보게 됩니다.

이 역시도 Javascript 엔진 마다 약간씩의 구현차이가 있어서 참조변수 이름을 표시해 주는 경우도 간혹 있습니다. 하지만 참조변수가 없는 callback 함수와 같은 것들은 죽었다 깨어나도 이름을 붙여줄 수가 없지요.

평소에 디버거에 별로 의존하지 않으시는 분들은 크게 걱정없이 AFE만을 사용하셔도 무방하겠지만, 디버거에 의존성이 높은 분들은 기왕이면 NFE로 코딩을 하시는편이 정신건강에 이로울것 같습니다.

그럼 디버깅 이야기는 여기서 마무리하고, 또다른 함수 표현식 이야기를 해 보겠습니다.
아래 코드를 한번 보시죠.

<script type="text/javascript">
    var module = ( function() {
        var somethingPrivate = function() {
            // 외부에 공개하지 않고 private하게 처리해야 하는 일들.
        };
        var initialize = function() {
            // 모듈 초기화 등등...
        };

        somthingPrivate();
        initialize();

        return function() {
            // 외부에 제공할 모듈 API등...
        };
    })();
</script>

Self-invoking Function의 단순한 좋은 예가 마땅히 떠오르지 않아 모듈 패턴이라고 불리우는 형태를 잠시 빌려왔습니다. 디자인 패턴에 대한 이야기가 아니니 그냥 휘리릭 넘어가고 함수 정의 형태자체를 설명하겠습니다. 보통 공통모듈, xx모듈, OO모듈 등등 어떤 모듈을 만들어 배포할 때 그 모듈을 사용하기 위해 무조건 처음에 반드시 실행되어야 하는 로직들이 있을것입니다. 하지만 외부에서는 별도로 해당 로직들을 호출하지 못하게 해야할 때, 사용하면 아주 좋은 패턴입니다.

module이라는 객체참조변수는 마지막 return에서 돌려주는 AFE를 받게 됩니다. 결국 somthingPrivate, initialize 함수 두개는 module이라는 변수를 통해서는 접근을 하지 못하게 됩니다. 대신에 위 스크립트가 해석될 때 딱 한번 실행이 됩니다. 그러면 module을 사용하고자 하는 사람들은 따로 초기화 등을 호출할 필요없이 그저 Open된 API를 활용하면 됩니다.

Self-invoking Function은 지난번 다른 포스팅( javascript: Scope of variables and functions ; 변수와 함수의 범위 )에서 별도의 Local Scope을 만들때도 사용해 봤었는데요, 사실 그렇게 사용하기 보다는 이렇게 디자인 패턴에 사용이 되어지는게 더 주된 용도입니다.

정리

아주 짤막하게 함수의 정의 방법과 각 유형별 특징들을 살펴봤습니다. 물론 이 포스팅에 모든것이 다 나온것은 아니지만, 각 방법들에 대한 주요특징들과 차이점 등은 잘 이해하셨으리라 생각됩니다.

FD이던 FE이던 함수의 구현부를 결정하는 알고리즘이 사실 더 중요할 수도 있겠지만, 일반적인 협업환경에서 사실 어떤 정의방법이 “정답이다.” 라고 말하기는 어렵습니다. 뜻하지 않은 Function Overriding 가능성을 줄이고, 가독성을 높이고, 코드의 중복을 최소화 하고, 기능별로 모듈화해서 모듈별 응집성을 높여서 유지보수성을 높이는 방향으로 함수를 정의하고 사용하는것이 목표가 아닐까요?

읽어주셔서 감사드리구요,
이번 포스팅은 여기서 마무리하겠습니다.

Written with StackEdit.


  1. 위키백과 ECMAScript 정의 참고