기본 콘텐츠로 건너뛰기

자바스크립트 함수, 선언부터 클로저와 this까지 한 번에 이해하기

자바스크립트 함수, 선언부터 클로저와 this까지 한 번에 이해하기

빠른 답

  • 자바스크립트에서 함수는 값처럼 다룰 수 있는 일급 객체라서 전달, 반환, 저장이 모두 가능합니다.
  • 함수 선언식과 함수 표현식은 호이스팅 방식이 다르므로 호출 가능 시점이 달라집니다.
  • 클로저는 함수가 자신이 만들어질 때의 스코프를 기억하는 동작이며, 상태 캡슐화의 핵심 도구입니다.
  • 화살표 함수는 문법만 짧은 것이 아니라 this와 arguments를 자체적으로 만들지 않는다는 점이 중요합니다.

한눈에 비교

Point 1
함수 선언식: 평가 단계에서 함수 객체까지 준비되므로 선언 전 호출이 가능하다.
Point 2
함수 표현식: 변수 바인딩과 함수 값 할당이 분리된다. const 는 TDZ 때문에 ReferenceError , var 는 undefined 상태라 TypeError 가 난다.
Point 3
화살표 함수: 함수 값이긴 하지만 자체 this , arguments , prototype 이 없고 생성자로 쓸 수 없다.
Point 4
클로저: 외부 값을 “복사”해 두는 게 아니라, 선언 시점의 스코프 체인에 계속 연결된다.
Point 5
undefined , ReferenceError , TypeError 는 모두 다른 신호다. 값이 없는지, 접근이 금지됐는지, 타입이 맞지 않는지 구분해서 읽어야 한다.

함수를 이해할 때는 문법 이름보다 먼저 네 가지를 보면 된다. 값처럼 전달되는가, 언제 호출 가능한가, 어떤 스코프를 기억하는가, this를 스스로 가지는가. 이 기준이 잡히면 선언식, 표현식, 클로저, 화살표 함수가 따로 놀지 않는다.

실행 환경을 먼저 맞추기

this와 전역 바인딩은 실행 환경에 따라 결과가 달라질 수 있다. 브라우저의 일반 스크립트, 브라우저의 ES 모듈, Node.js CommonJS, Node.js ES 모듈은 top-level this가 서로 같지 않다. 같은 코드를 따라 쳤는데 출력이 다르다면 함수 개념이 틀린 게 아니라 실행 환경이 섞였을 가능성이 높다.

실습을 고정하려면 Node.js를 ES 모듈 기준으로 맞춰 두는 편이 좋다.

{
  "type": "module",
  "scripts": {
    "demo": "node demo.js"
  }
}

이렇게 두면 파일 전체가 모듈 문맥으로 실행되고, this와 스코프를 비교할 때 브라우저의 오래된 일반 스크립트 규칙이 끼어들지 않는다. 특히 아래 예시처럼 “내부 일반 함수의 this가 왜 undefined인가”를 볼 때 해석이 훨씬 선명해진다.

함수는 값이면서 호출 가능한 객체다

함수는 코드 묶음이기 전에 값이다. 그래서 변수에 넣을 수 있고, 배열에 넣을 수 있고, 다른 함수에 넘길 수 있다. 동시에 일반 객체와 달리 호출할 수 있다는 점이 핵심이다. “함수는 객체다”와 “모든 객체는 함수다”는 전혀 다른 말이다.

아래 예시는 함수가 값처럼 흘러다니고, 선언 방식에 따라 호출 가능 시점이 어떻게 갈리는지 한 번에 보여준다.

const run = (fn, input) => fn(input);
const square = n => n * n;

console.log(typeof square);
console.log(run(square, 4));
console.log(declared(1, 2));

try {
  console.log(expressedConst(1, 2));
} catch (error) {
  console.log(`${error.name} - ${error.message}`);
}

try {
  console.log(expressedVar(1, 2));
} catch (error) {
  console.log(`${error.name} - ${error.message}`);
}

function declared(a, b) {
  return a + b;
}

const expressedConst = function add(a, b) {
  return a + b;
};

var expressedVar = function (a, b) {
  return a + b;
};

console.log(expressedConst.name);

여기서 run(square, 4)는 함수가 값이라는 사실을 가장 단순하게 드러낸다. square는 전달되고, run 안에서 호출되며, 결과는 다시 값으로 반환된다. 콜백, 이벤트 핸들러, 미들웨어, 고차 함수가 자연스럽게 성립하는 이유가 바로 이것이다.

또 하나 중요한 점은 “함수 표현식”과 “익명 함수”를 같은 말로 보면 안 된다는 것이다. const expressedConst = function add() {}는 함수 표현식이지만 익명 함수가 아니다. 이름 있는 함수 표현식은 디버깅할 때 스택 트레이스와 name 프로퍼티에서 차이를 만든다.

선언식, 표현식, 화살표 함수의 차이

함수 선언식은 문장이고, 함수 표현식은 값이다. 화살표 함수는 함수 표현식의 한 형태지만 런타임 의미가 일반 함수와 완전히 같지 않다. 이 차이를 문법 취향 정도로 보면 이후에 this, arguments, 생성자 호출에서 계속 헷갈린다.

실무에서 자주 틀리는 기준만 좁혀 보면 이렇다.

  • 선언식은 파일 어디서든 먼저 읽히는 유틸리티 함수에 잘 맞는다.
  • 표현식은 함수 자체를 값으로 조합하거나 조건부로 전달할 때 자연스럽다.
  • 화살표 함수는 짧은 콜백과 바깥 this 유지에 강하다.
  • 메서드 문맥, 생성자 문맥, arguments가 중요한 자리는 일반 함수가 더 안전하다.

용어 기준을 더 확인하고 싶다면 MDN의 Function declaration, Function expression, Arrow function expressions, Closures를 함께 보면 좋다.

호이스팅과 TDZ를 오류 이름으로 읽기

호이스팅은 코드가 물리적으로 위로 올라간다는 뜻이 아니다. 실행 전에 식별자 바인딩이 준비된다고 이해해야 실제 동작과 맞다. 이 관점으로 보면 왜 같은 “선언 전 호출”인데도 오류가 다르게 나오는지 설명할 수 있다.

위 코드에서 declared(1, 2)는 성공한다. 함수 선언식은 평가 단계에서 함수 객체까지 준비되기 때문이다. 반면 expressedConst(1, 2)ReferenceError가 난다. 식별자를 아예 모르는 게 아니라, 바인딩은 있지만 아직 초기화되지 않았기 때문이다. 이것이 letconst의 TDZ, 즉 일시적 사각지대다.

expressedVar(1, 2)는 또 다르다. 여기서는 TypeError가 난다. var는 선언 시점에 undefined로 초기화되므로 “읽기는 되지만 호출할 값이 아님”이라는 상태가 된다. 즉 ReferenceError는 접근 시점 문제이고, TypeError는 읽어 온 값의 타입 문제다. 둘을 구분해 읽으면 디버깅 속도가 크게 빨라진다.

많이 놓치는 포인트도 하나 있다. TDZ 안의 constlet에 대해 typeof를 써도 안전하지 않다. 이 경우에도 ReferenceError가 날 수 있다. “typeof는 항상 안전하다”는 오래된 감각은 TDZ 앞에서 깨진다.

클로저는 값을 복사하지 않고 환경을 붙잡는다

클로저는 외부 값을 복사해 보관하는 기능이 아니다. 함수가 만들어질 때의 렉시컬 환경에 계속 접근하는 구조다. 그래서 같은 팩토리 함수를 여러 번 호출하면 모양은 같아 보여도 서로 다른 상태를 가진 함수들이 생긴다.

function createCounter(start = 0) {
  let count = start;

  return {
    up() {
      count += 1;
      return count;
    },
    current() {
      return count;
    },
  };
}

const left = createCounter(1);
const right = createCounter(10);

console.log(left.current());
console.log(left.up());
console.log(left.up());
console.log(right.up());

const ArrowCtor = () => {};
try {
  new ArrowCtor();
} catch (error) {
  console.log(`${error.name} - ${error.message}`);
}

const obj = {
  value: 42,
  method(label) {
    console.log("method this.value:", this.value);

    function normal() {
      console.log("normal inner this:", this?.value);
    }

    const arrow = () => {
      console.log("arrow inner this:", this.value);
      console.log("arrow outer arguments[0]:", arguments[0]);
    };

    normal();
    arrow();
  },
};

obj.method("first");

leftright는 같은 createCounter에서 나왔지만 서로 다른 렉시컬 환경을 가진다. 그래서 left를 두 번 올려도 right에는 영향이 없다. 이 때문에 클로저는 상태 캡슐화, 설정 고정, 팩토리 함수 패턴에서 강력하다.

동시에 오해도 많다. 클로저는 “옛날 값을 얼려 두는 기능”이 아니다. 같은 환경을 계속 바라보므로 외부 상태가 바뀌면 내부 함수가 보는 값도 함께 달라진다. 값 복사와 참조 유지의 차이를 혼동하면 비동기 코드에서 특히 자주 틀린다.

this와 arguments는 호출 방식과 선언 방식이 가른다

this는 함수가 어디에 적혀 있는지가 아니라 어떻게 호출됐는지에 더 크게 좌우된다. 반면 클로저는 어디에서 선언됐는지가 핵심이다. 둘 다 함수와 함께 등장하지만 규칙은 전혀 다르다.

위 코드에서 obj.method("first")method는 메서드 호출이므로 thisobj를 가리킨다. 하지만 그 안의 normal()은 그냥 일반 함수 호출이기 때문에 모듈의 엄격 모드 기준으로 thisundefined다. 그래서 this?.value는 예외 없이 undefined를 찍는다. 만약 ?. 없이 this.value를 읽었다면 여기서는 TypeError가 났을 것이다.

반대로 arrow는 자신의 this를 만들지 않는다. 바깥 methodthis를 그대로 닫아 두기 때문에 42를 읽는다. arguments도 마찬가지다. 화살표 함수 내부의 arguments는 그 함수 자신의 것이 아니라 바깥 일반 함수 methodarguments를 참조한다. 그래서 "first"가 출력된다.

new ArrowCtor()TypeError를 내는 이유도 같은 축에서 이해할 수 있다. 화살표 함수는 생성자 역할을 하도록 설계되지 않았기 때문에 new 대상이 될 수 없다. 화살표 함수는 짧아서 좋은 게 아니라, 호출 문맥을 고정하고 싶을 때 좋은 것이다.

출력으로 읽는 디버깅 포인트

함수 관련 버그는 에러 이름과 출력 형태만 제대로 읽어도 절반은 정리된다. 아래는 앞선 예시를 실행했을 때 볼 수 있는 대표적인 출력이다.

function
16
3
ReferenceError - Cannot access 'expressedConst' before initialization
TypeError - expressedVar is not a function
add
1
2
3
11
TypeError - ArrowCtor is not a constructor
method this.value: 42
normal inner this: undefined
arrow inner this: 42
arrow outer arguments[0]: first

이 출력은 이렇게 해석하면 된다.

  • function: 함수가 값으로 평가된다는 뜻이다.
  • 16: 함수를 다른 함수에 넘겨 실행했다는 뜻이다.
  • 3: 선언식은 선언 전 호출이 가능하다는 뜻이다.
  • ReferenceError: 바인딩은 있지만 아직 접근할 수 없다는 뜻이다.
  • TypeError - ... is not a function: 값을 읽긴 했지만 호출 가능한 타입이 아니라는 뜻이다.
  • add: 함수 표현식은 익명일 수도 있고 이름을 가질 수도 있다는 뜻이다.
  • 1, 2, 3, 11: 같은 팩토리에서 나온 함수라도 각자 다른 상태를 붙잡는다는 뜻이다.
  • ... is not a constructor: 화살표 함수는 생성자가 아니라는 뜻이다.
  • normal inner this: undefined: 일반 함수의 this는 호출 방식에 따라 끊긴다는 뜻이다.
  • arrow inner this: 42: 화살표 함수는 바깥 this를 유지한다는 뜻이다.

함수 디버깅에서 흔한 오해는 세 가지다. “호이스팅 때문에 함수는 다 미리 쓸 수 있다”, “화살표 함수가 더 최신이니 항상 더 좋다”, “클로저는 외부 값을 복사한다”는 모두 틀린 문장이다. 실제로는 바인딩 시점, 호출 문맥, 렉시컬 환경이라는 서로 다른 규칙이 동시에 작동한다.

실무에서 어떤 형태를 고를까

선택 기준은 단순하다. 이름이 드러나는 공용 유틸리티면 선언식이 읽기 좋고, 함수를 조합하거나 전달해야 하면 표현식이 유연하다. 짧은 콜백이나 바깥 this를 유지해야 하는 비동기 코드라면 화살표 함수가 편하다. 반대로 메서드 문맥을 직접 다루거나 arguments와 생성자 의미가 중요하면 일반 함수가 더 안전하다.

결국 자바스크립트 함수는 “문법”보다 “의미”로 봐야 한다. 값으로 다뤄지는가, 초기화 시점이 언제인가, 어떤 환경을 기억하는가, 호출 문맥이 어떻게 결정되는가. 이 네 가지를 먼저 보면 선언식, 표현식, 클로저, 화살표 함수가 각각 따로 외울 대상이 아니라 하나의 런타임 규칙으로 묶인다.

원문 참고

https://www.maeil-mail.kr/question/33

댓글

이 블로그의 인기 게시물

아이콘 폰트 (icomoon 사용법)

 장난감 프로젝트를 만들다 보면, 아이콘이 필요한 경우가 있다. 간단하게 아이콘을 인터넷에서 검색하여, 이미지로 넣어두고 이미지 태그를 이용하여, 사용하는 경우가 일반적이였지만...  요즘에는 대부분 폰트를 이용하여 아이콘을 노출 한다. 나 같은 경우에도 기본적으로  https://material.io/resources/icons 를 참고하여 아이콘 폰트를 이용할 수 있도록 처리하고, 추가적으로 필요한 아이콘이고, 일상적으로 사용 되지 않는 아이콘의 경우에는  https://icomoon.io 에서 제작하여, 아이콘 폰트로 이용 하곤 한다.  그래서 이번에는 아이콘  https://icomoon.io 의 사용법을 간단히 공유하고자 한다.   들어가자 마자 위의 icoMoonApp버튼을 누르면 아래와 같은 화면이 나타난다.  icomoon에서 무료로 제공하는 아이콘들이 보이면 위에 파란색으로 표시 되어있는 집 모양 세가지를 선택한 후, 아래의 빨간색으로 표시되어있는 Generate Font를 눌러보자.  그리고 나서 바로 다운로드를 요청해보자. icomoon.zip이 다운로드가 될텐데, 압축을 해제해 보면, 아래의 폴더 및 파일들이 있다. 아래에서 중요한 것은 font 폴더와 style.css이다. demo-files fonts demo.html Read Me.txt selection.json style.css <!doctype html > <html> <head> <link rel ="stylesheet" href ="style.css" ></head> </head> <body> <span class ="icon-home" ></span> <span class ="icon-home2" ></span> <span class ="icon-hom...

Chart js와 amchart 비교

Chart js 특징은 위의 그림으로 대체 할 수 있을 듯 하다. 오픈 소스이고, 기본으로 제공하는 차트 종류가 8가지 Canv a s를 이용해서 차트를 그리고, 반응형을 지원한다. amchart amchart는 기본적으로 유료이며, 기본으로 제공하는 차트 종류가 기본적인 차트 + 주식 처럼 보이는 차트 + 지도에 관련된 차트(?) 까지 하면, 기본 제공 하는 종류가 20개 내외 이려나, 일일이 세기에는 양이 좀 많아 보인다. 렌더링은 svg를 통하여 그려지고, 당연 반응형도 지원이 된다. 그러면, 이 둘중에 어떤것이 내 프로젝트에 적합 하냐는 것이 문제이다. 일단, 주식 처럼 보이는 차트나 지도에 관련된 차트(?)가 필요하면, amchart를 선택해야 되는 것은 맞다. 그건 당연한 것이니 빼고 얘기 해보자! 여러 종류의 차트가 필요하다면, 일단은 amchart를 염두해 두는 것이 좋다. 돈 낸 만큼은 하는 듯 하다. 하지만, 기본적인 막대 그래프, 도넛 차트 등, 아주 기본적인 차트들인데, Chart js도 amchart도 그러한 차트가 없을 때가 문제가 된다. 그렇다면, 조금이라도 커스텀이 용이한 것을 찾는 것이 좋을 것이다.  일단 amchart에서 custom이라고 검색 하였을 때, 검색 결과가 61가지가 나온다. 차트의 종류도 많고, 각 차트마다 들어가는 속성이 매우 많기 때문에, 웬만한 내용들은 속성 값을 어떻게 주느냐에 따라서 변경이 가능 하게 된다. 커스텀의 예를 들면, 기본적으로 도넛 파이의 형태를 띄면서, 화살표로 목표를 표시해주는 차트가 필요하다고 생각 해보자. 이것은 amchart로 만든 그래프이고 이것은 chart js로 만든 그래프이다. 모양이 살짝 다르긴 하지만, 완벽하게 똑같이 구현 할 수도 있다. amchart로 만든 그래프의 경우, 저것은 도넛그래프가 아닌 guage 그래프이다. 원래 게이지 그래프는 이와 같...

javascript 압축 파일 다운로드

이번에는 전 게시글의 응용판? 이라고 해야하나....? 어쨋든! 우리는 각각의 파일들을 다운로드 해보았다. 그런데 생각보다 귀찮음?을 느꼇을 것이다. 파일을 각각 다운 받아야 한다는 현실때문에! 그래 파일 두개야 뭐 그렇다 치지... 하지만, 개발자도 사용자도 게으름뱅이이다. 자 결국, 우리가 해야 하는 것은 파일을 한 번에 둘다! 다운 받는 것이다. 물론, 클릭 한번에 여러개의 함수를 엮어서 다운받게 하면 되지만! 크롬에서 자주 봤듯이, 여러개의 파일을 다운로드를 시도하면 <- 여러개의 파일을 다운로드 합니다. 허용 합니까? 하고 물어보는 것을 볼 수 있다. 게다가 다운로드 한 파일들을 찾기도 귀찮다는 것. 자 해결책을 제시해보자면, https://github.com/Stuk/jszip 클라이언트 단에서 파일을 zip파일로 압축을 할 수가 있다! 필요한 작업은 아래와 같다. 0. 데이터 준비 1. BLOB(binary large object)를 만든다. 2. Blob을 URL.createObjectURL을 사용하여, 해당 binary의 주소를 생성. 3. 다운로드가 필요한 파일들을 Zip 객체에 셋팅! 4. a태그를 이용하여, 해당 url 셋팅 하고, 다운로드. 전 게시물과 별로 달라진게 없네... 자 그럼 샘플! 샘플을 보자! http://embed.plnkr.co/NMprnRxqYG0fkHa2J55D/ var util = {} function fixBinary(bin) { //binary to arrayBuffer var length = bin.length var buf = new ArrayBuffer(length) var arr = new Uint8Array(buf) for (var i = 0; i < length; i++) { arr[i] = bin.charCodeAt(i) } return buf } window.onload = function() { ...