⚙️

[JS] 함수(Functions)


함수는 자바스크립트 프로그래밍의 핵심이다. 하나의 프로그램과 하나의 값을 묶은 개념은 다양한 용도로 사용될 수 있다.

함수 정의하기

가장 기본적인 함수 정의는 변수의 값을 함수로 주는 방법이다. 이 방법을 '함수 표현식'으로 정의한다고 한다. function 키워드로 시작하는 표현식에 parameters(매개 변수)와 함수가 호출되면 실행될 구문들이 포함된 body가 있다. 이런 방식으로 작성된 함수의 'body'는 항상 중괄호(braces)로 묶여야 한다.

1const square = function(x) {
2 return x * x;
3};

함수의 return값으로 아무 표현식이 없으면 undefined가 반환된다. return이 없는 함수의 경우도 undefined가 반환된다.

함수 값은 다른 값과 사용 방법이 동일하다. 즉, 아무 표현식에서나 사용할 수 있고 새 변수에 함수 값을 저장하거나 다른 함수에 인수로 전달하는 등 작업이 가능하다.

선언 방법

처음에 보여줬던 방법보다 더 빠르게 생성하는 방법이 있다. 바로 '함수 선언'(declaration)이다. 아래와 같은 방법을 '함수 선언문'으로 함수를 선언한다고 한다.

1function square(x) {
2 return x * x;
3}

함수를 선언하는데는 함수 정의와 중요한 차이점이 있다.

1console.log("미래에서 보내는 메시지:", future());
2
3function future() {
4 return "아직도 하늘을 나는 자동차는 없네요ㅎㅎ;";
5}

바로 함수를 사용하는 코드의 아래쪽에 함수가 정의돼 있어도 동작한다는 것이다(호이스팅)! 함수 선언은 일반적인 'top-to-bottom' 흐름에 속하지 않는다.

화살표 함수(Arrow Function)

function 키워드를 사용하지 않고 이름 그대로 화살표(=>)를 사용한다.

1const square = (x) => {
2 return x * x;
3};

문법을 보면 "이것을 입력하면 이 결과가 나온다"라고 읽을 수 있을 것 같다.

매개변수가 하나만 존재하는 경우 주위 괄호를 생략할 수 있다. 매개변수가 없다면 빈 괄호를 사용한다. 본문이 중괄호 안에 있는 블록이 아닌 단일 표현식이면 함수에서 해당하는 표현식을 반환해준다. 위에 나왔던 식을 바꿔 아래와 같이 표현할 수 있다.

1const square = x => x * x;

선택적 인수(Optional Arguments)

자바스크립트는 관대하다. 함수에 너무 많은 인수를 보내도, 인수를 좀 덜 보내도 동작한다.

1function square(x) { return x * x; }
2console.log(square(4, true, "인수 넣을게"));
3// -> 16

이것의 단점으로는 실수로 함수에 잘못된 개수의 인수를 전달해서 에러가 발생해도 아무도 알려주지 않는다는 것이다. 단점이 있으면 장점도 있는 법. 이 같은 동작을 사용해서 다양한 개수의 인수로 함수를 호출할 수 있다. 아래 예를 보자.

1function minus(a, b) {
2 if (b === undefined) return -a;
3 else return a - b;
4}

아래 방식으로도 사용할 수 있다.

1function power(base, exponent = 2) {
2 let result = 1;
3 for (let count = 0; count < exponent; count++) {
4 result *= base;
5 }
6 return result;
7}

두 번째 인수가 제공되지 않으면 2가 기본값으로 설정된다.

변수와 범위(Scope)

모든 변수에는 범위가 있다. 범위(Scope)는 변수를 식별할 수 있는 프로그램의 영역을 말한다.

함수나 블록 밖에서 정의된 변수의 경우 프로그램 전체에서 사용될 수 있게 된다. 전역에서 사용할 수 있으니, 이것을 'global binding(전역 변수)'이라고 한다.

함수의 파라미터나 함수 내부에서 선언한 변수는 해당 함수에서만 참조할 수 있게 된다. 이것을 'local binding(지역 변수)'이라고 한다. 지역 변수는 함수가 호출될 때마다 새로운 인스턴스가 생성된다.

let이나 const키워드로 생성된 변수는 'block'({}) 스코프를 가진다. 2015년 이전의 자바스크립트에서는 함수에서만 새 범위를 만들었기 때문에, var키워드로 만든 변수는 같은 함수 내부라면 어떤 블럭 안에 있어도 사용할 수 있게 된다.

1let x = 10;
2if (true) {
3 let y = 20;
4 var z = 30;
5 console.log(x + y + z); // -> 60
6}
7// y는 사용될 수 없다.
8console.log(x + z); // -> 40

Lexcial Environment

자바스크립트에선 실행 중인 함수, 코드 블록, 스크립트 전체는 렉시컬 환경(Lexical Envirionment)이라 불리는 특수 내부 객체를 갖는다.

랙시컬 환경 객체는 두 부분으로 구성된다.

  • 환경 레코드(Environment Record): 모든 지역 변수를 속성으로 가지는 객체다.
  • 외부 렉시컬 환경에 대한 참조: 외부 코드와 연관

변수는 특수 내부 객체인 '환경 레코드'의 속성일 뿐이다. 변수를 가져오거나 변경하는 것은 환경 레코드의 속성을 가져오거나 변경하는 것과 동일하다.

1/* execution start */ // {phrase: <uninitialized>}
2let phrase; // {phrase: undefined}
3let phrase = "Hello"; // {phrase: "Hello"}

렉시컬 환경은 명세서에서 자바스크립트가 어떻게 동작하는지 설명하는 데 쓰이는 '이론상의 객체'이기 때문에, 코드를 사용해 직접 렉시켤 환경을 얻거나 조작하는 것은 불가능하다.

함수 선언으로 선언한 함수는 일반 변수와 다르게 바로 초기화된다(var 제외). 다른 말로, 렉시컬 환경이 만들어지는 즉시 사용할 수 있게 된다.

이게 '함수 선언'으로 선언한 함수를 사용하는 코드의 아래쪽에 함수가 정의돼 있어도 동작하는 이유다. 물론 let f = function(arg)... 같이 '함수 표현식'으로 함수를 정의하는 것은 let 처럼 실행 흐름이 let을 만나 선언이 되어야 사용 가능하다.

1/* execution start */ // {phrase: <uninitialized>, say: function}
2let phrase = "Hello"; // {phrase: "Hello", say: function}
3function say(name) {
4 console.log(`${phrase}, ${name}`);
5}
6say("John");

함수를 호출해 실행하면 새로운 렉시컬 환경이 자동으로 만들어진다. 렉시컬 환경엔 매개변수와 함수의 지역 변수가 저장된다.

함수가 호출 중인 동안엔, 호출 중인 함수를 위한 내부 렉시컬 환경내부 렉시컬 환경이 가리키는 외부 렉시컬 환경을 갖게 된다.

위의 예시의 6번 줄에서 함수가 호출되면 {name: "John"}을 가지는 내부 렉시컬 환경이 생기고, 내부 렉시컬 환경은 {phrase: "Hello", say: function}를 가지는 외부 렉시컬 환경을 가리킨다. 여기선 외부 렉시컬 환경이 전역 렉시컬 환경이다.

코드에서 변수에 접근할 땐 먼저 내부 렉시컬 환경을 검색 범위로 잡는다. 내부에서 찾지 못하면 외부로, 그리고 전역 렉시컬까지 확장하면서 반복된다.

클로저(Closure)

위에서 렉시컬 환경에 대해 알아봤다. 이를 바탕으로 '함수를 반환하는 함수'를 이해해보자.

1function wrapValue(n) {
2 let local = n;
3 return () => local;
4}
5
6let wrap1 = wrapValue(1);
7let wrap2 = wrapValue(2);
8console.log(wrap1());
9// -> 1
10console.log(wrap2());
11// -> 2

wrapValue()를 호출할 때마다 새로운 렉시컬 환경이 생성될 것이다. 그리고 새로운 렉시컬 환경에 필요한 변수들이 저장된다.

wrapValue()를 호출하면 nlocal을 저장하는 렉시컬 환경이 생성되고, 리턴하는 함수를 위한 렉시컬 환경이 또 생성될 것이다. 여기서 중요한 점이 있다. 모든 함수는 함수가 생성된 곳의 렉시컬 환경을 기억한다는 점이다. 함수는 [[Environment]]라는 숨김 속성을 갖는데, 여기에 함수가 만들어진 곳의 렉시컬 환경에 대한 참조가 저장된다.

따라서 wrap1.[[Environment]]에는 {n: 1, local: 1}이 있는 렉시컬 환경에 대한 참조가 저장된다. [[Environment]] 덕분에 호출 장소와 상관없이 함수는 자신이 태어난 곳을 기억할 수 있다. [[Environment]]는 함수가 생성될 때 딱 한 번 값이 세팅되고 영원히 변하지 않는다.

함수 호출이 끝나면 함수에 대응하는 렉시컬 환경은 메모리에서 제거된다. 하지만, 함수의 변수가 다른 렉시컬 환경으로부터 참조된다면 제거되지 않는다. [[Environment]] 속성에 외부 함수 렉시컬 환경에 대한 정보가 저장되기 때문에 도달 가능한 상태가 된다. 함수 호출이 끝났지만 렉시컬 환경이 메모리에 유지되는 이유가 바로 이 때문이다. 자세한 내용은 가비지 콜렉션에서 다룬다.

이렇게 함수와 그 함수가 선언된 렉시켤 환경의 조합클로저(closure)라고 한다. 함수가 만들어졌을 때 그 환경을 기억하는 기능이다. 클로저 덕분에 변수의 수명(lifetime)에 대한 걱정을 덜 수 있게 된다.


참조:
Eloquent JavaScript
ko.javascript.info