화살표 함수와 this
화살표 함수의 축약 문법과 외부 this를 캡처하는 특성을 익히고 메서드·생성자처럼 피해야 할 위치를 구분합니다.
This 키워드와 바인딩에서 일반 함수의 this는 호출하는 방식에 따라 정해진다는 것을 배웠습니다. 이 특성 때문에 콜백이나 중첩 함수에서 this가 바뀌어 버리는 일이 잦았고, 이를 피하려고 bind()나 const self = this 같은 우회 방법을 자주 썼습니다.
ES2015(ES6)에 추가된 화살표 함수(arrow function)는 이 문제를 간단하게 풉니다. 화살표 함수는 자기 this를 만들지 않고 바깥의 this를 그대로 쓰므로, 바깥의 this를 유지해야 하는 콜백을 짧게 쓸 수 있습니다. 반대로 호출한 객체를 this로 받아야 하는 메서드에는 맞지 않습니다. 지금의 자바스크립트 코드에서는 화살표 함수를 거의 어디서나 볼 수 있으므로, 이 글에서 문법과 this의 차이, 쓰지 말아야 할 곳까지 정확하게 정리합니다.
화살표 함수의 기본 문법
화살표 함수는 function 키워드 대신 화살표 =>로 함수를 정의하는 짧은 문법입니다. 기본 형태는 (매개변수) => 표현식 또는 (매개변수) => { 함수 본문 }입니다.
// 기존 함수 표현식
const add = function(a, b) {
return a + b;
};
// 화살표 함수 (기본 형태)
const addArrow = (a, b) => a + b; // 중괄호와 return이 생략됨
console.log(addArrow(2, 3)); // 결과: 5본문이 표현식 하나라면 중괄호와 return을 생략할 수 있고, 그 표현식의 값이 그대로 반환됩니다. 몇 가지 형태를 더 살펴봅니다.
매개변수가 하나라면 괄호를 생략할 수 있습니다.
const greet = name => `Hello, ${name}!`;
console.log(greet("Alice")); // 결과: Hello, Alice!본문도 식 하나뿐이라 중괄호와 return을 생략했으므로, 템플릿 리터럴의 값이 그대로 반환됩니다.
매개변수가 없다면 빈 괄호를 꼭 써야 합니다. 괄호 없이 =>로 바로 시작하면 문법 오류가 납니다.
const sayHi = () => "Hi there!";
console.log(sayHi()); // 결과: Hi there!본문을 중괄호로 감싼 블록 본문에서는 값을 돌려주려면 return을 직접 적어야 합니다. 출력만 하는 함수처럼 돌려줄 값이 없다면 return을 생략할 수 있으며, 그때 결과는 undefined입니다.
const calculateSum = (numbers) => {
let sum = 0;
for (let num of numbers) {
sum += num;
}
return sum;
};
console.log(calculateSum([1, 2, 3, 4, 5])); // 결과: 15객체 리터럴을 바로 돌려줄 때는 소괄호로 감싸야 합니다. 감싸지 않으면 중괄호가 객체가 아니라 함수 본문으로 해석되기 때문입니다.
const createUser = (name, age) => ({ name: name, age: age });
console.log(createUser("Bob", 28)); // 결과: { name: 'Bob', age: 28 }화살표 함수의 this
화살표 함수를 쓰는 가장 큰 이유는 this를 다루는 방식입니다. function으로 정의한 일반 함수는 호출할 때마다 this가 정해지지만, 화살표 함수는 자기 this를 만들지 않고 자신을 정의한 바깥 스코프의 this를 씁니다. 화살표 함수의 this는 호출하는 방식이 아니라 정의한 위치로 정해지므로, 렉시컬 this(lexical this)라고 부릅니다.
DECISION TREE · LEXICAL BINDINGS
화살표 함수의 lexical this 경계
화살표 함수는 호출 대상의 this를 새로 만들지 않고 바깥 값을 사용한다. 동적 this나 생성자 호출이 필요한지 먼저 확인한다.
- own binding이나 new가 필요한가?
dynamic this, own arguments/new.target, 동적 this가 필요하면 일반 함수·메서드를 쓴다. new 호출에는 생성 가능한 function 또는 class가 필요하다.
- enclosing binding을 유지할 것인가?
그렇다면 arrow를 쓴다. arrow에는 own binding이 없고 this/arguments/super/new.target을 바깥에서 resolve한다.
- parameter만 쓰는 callback인가?
둘 다 의미가 맞다. lexical capture가 필요하지 않다면 arrow는 간결성 선택이다.
DOM listener
listener의 dynamic this가 필요하면 일반 함수, arrow를 쓰면 event.currentTarget을 명시한다.
class member
prototype method는 공유된다. field arrow는 this를 자동 보존하지만 instance마다 함수 객체를 만든다.
this는 호출 대상이 아닌 바깥에서 결정
화살표 함수는 자체 this를 갖지 않는다. 객체의 메서드로 호출하거나 call·bind로 호출 대상을 바꿔도 그 this를 새로 설정하지 않는다.
this를 고정하는 화살표 함수에는 자체 arguments와 생성자 기능이 없다. DOM의 event.currentTarget 사용과 클래스 필드의 인스턴스별 함수 비용까지 확인한다.
위 그림은 함수의 형태를 고르는 두 가지 질문을 보여 줍니다. 먼저 호출할 때마다 새로 정해지는 this나 자기만의 arguments가 필요한지, 또는 new로 호출해야 하는지를 묻습니다. 그렇다면 일반 함수나 메서드를 쓰고, new가 필요하면 생성자로 쓸 수 있는 function이나 class를 씁니다. 그렇지 않다면 바깥의 this를 유지해야 하는지를 묻고, 유지해야 하면 화살표 함수를 씁니다. this 없이 인수만 쓰는 콜백이라면 어느 쪽이든 동작하므로 의도에 맞게 고르면 됩니다.
그림의 아래쪽은 화살표 함수가 this를 어디서 가져오는지를 따로 보여 줍니다. 화살표 함수는 자신을 감싼 바깥 일반 함수의 실행에서 this를 가져오며, 그 바깥 함수가 메서드로 호출되었다면 this는 그 객체입니다. 객체 리터럴은 this를 만드는 범위가 아니라서, 객체 리터럴 안에 바로 적은 화살표 함수는 객체를 건너뛰고 더 바깥의 this를 씁니다. 또 화살표 함수의 this는 call()이나 bind()로 바꿀 수 없습니다.
그림의 설명 카드는 두 가지를 더 짚습니다. DOM 이벤트 리스너에서 리스너가 등록된 요소를 this로 받으려면 일반 함수를 쓰고, 화살표 함수를 쓴다면 event.currentTarget으로 요소를 읽습니다. 클래스의 메서드는 프로토타입에 하나만 있어 모든 인스턴스가 공유하지만, 클래스 필드에 화살표 함수를 넣으면 this가 자동으로 유지되는 대신 인스턴스마다 함수 객체가 따로 만들어집니다.
일반 함수의 this
먼저 일반 함수로 지연 인사를 구현해 봅니다. 다음 person 객체에는 바로 인사하는 sayHello와 0.5초 뒤에 인사하는 greetDelayed 두 메서드가 있습니다. 각 메서드가 this.name과 self.name 중 무엇을 읽는지, 주석 처리된 WRONG 줄은 무엇을 읽게 되는지 비교해 보세요.
const person = {
name: "Minjun",
// 1. 메서드로서 호출: `.` 앞의 객체(person)가 `this`가 됨
sayHello: function() {
console.log(`Hello, my name is ${this.name}.`);
},
// 2. 중첩 함수에서 `this` 문제 발생
greetDelayed: function() {
// 기존 함수의 `this` (person)를 유지하기 위한 꼼수: `self = this`
const self = this;
setTimeout(function() { // 브라우저 timer가 일반 콜백의 this로 window를 전달함 (strict 여부와 무관)
console.log(`Delayed Hello, my name is ${self.name}. (using self)`);
// console.log(`Delayed Hello, my name is ${this.name}. (using this - WRONG!)`);
}, 500);
}
};
person.sayHello(); // 결과: Hello, my name is Minjun.
person.greetDelayed(); // 결과: Delayed Hello, my name is Minjun. (using self)
// WRONG! 줄을 켜면 브라우저의 window.name을 읽으며 person.name을 읽지 않음person.sayHello()는 메서드 호출이라 this가 person이고 이름이 제대로 출력됩니다. 문제는 greetDelayed() 안의 setTimeout() 콜백입니다. 브라우저의 타이머는 일반 함수 콜백을 호출할 때 this로 window를 넘기므로, 콜백 안의 this.name은 person.name이 아니라 window.name을 읽습니다. 콜백이 엄격 모드여도 타이머가 window를 넘기는 것은 같습니다. 그래서 예전에는 바깥의 this를 self 같은 변수에 담아 두고 콜백에서 그 변수를 읽는 방법을 썼습니다.
화살표 함수의 this
같은 일을 화살표 함수로 하면 self가 필요 없습니다. 다음 코드는 화살표 함수를 두 자리에 씁니다. greetDelayed 안의 setTimeout() 콜백은 올바르게 쓴 예이고, sayHello 메서드 자체를 화살표 함수로 만든 것은 흔히 틀리는 예입니다.
const personArrow = {
name: "Seulgi",
sayHello: () => { // 브라우저 classic script의 최상위라면 this는 window; ES module이면 undefined
console.log(`Hello, my name is ${this.name}. (WRONG! - window or undefined)`);
},
greetDelayed: function() { // 이 함수는 일반 함수이므로 `this`는 `personArrow`를 가리킴
// 화살표 함수는 자신의 상위 스코프(greetDelayed)의 `this`를 가져옴
setTimeout(() => {
console.log(`Delayed Hello, my name is ${this.name}. (using arrow function)`);
}, 500);
}
};
personArrow.sayHello(); // classic script에서 window.name이 빈 문자열이면 Hello, my name is . (WRONG! - window or undefined)
// ES module에서는 this.name 접근이 TypeError; 객체 리터럴은 자체 this 스코프가 아님
personArrow.greetDelayed(); // 결과: Delayed Hello, my name is Seulgi. (using arrow function)setTimeout()에 넘긴 화살표 함수는 자기 this를 만들지 않고, 자신을 정의한 greetDelayed() 호출의 this를 씁니다. greetDelayed()는 personArrow.greetDelayed()로 메서드 호출되었으므로 그 this는 personArrow이고, 화살표 함수 안의 this.name도 "Seulgi"가 됩니다.
반면 sayHello는 객체 리터럴에 바로 화살표 함수로 적었기 때문에 personArrow를 this로 받지 못합니다. 브라우저의 classic script에서는 바깥인 전역 코드의 this, 즉 window를 써서 이름 자리가 비어 출력됩니다. ES 모듈이라면 최상위의 this가 undefined라서 this.name을 읽는 순간 TypeError가 나고, 뒤의 greetDelayed()까지 실행되지 않습니다. 모듈에서 두 결과를 비교하려면 첫 호출을 주석으로 막거나 try...catch로 감싸 실행하세요.
화살표 함수를 쓰지 말아야 할 곳
화살표 함수는 편리하지만 일반 함수를 모든 곳에서 대신할 수는 없습니다. this, arguments, new를 다루는 방식이 다르기 때문에, 다음 경우에는 일반 함수를 써야 합니다.
객체의 메서드
객체 리터럴에 화살표 함수로 메서드를 적으면, 그 메서드의 this는 객체가 아니라 바깥의 this입니다. 브라우저 classic script의 최상위에서는 window이고, ES 모듈의 최상위에서는 undefined입니다.
const myObject = {
value: 10,
// BAD: 화살표 함수는 바깥 this 사용 (classic script: window, ES module: undefined)
getValueArrow: () => {
console.log(this.value); // classic script: window.value; ES module: TypeError
},
// GOOD: 일반 함수는 `.` 호출 시 myObject를 this로 바인딩
getValueNormal: function() {
console.log(this.value); // 10
}
};
myObject.getValueArrow();
myObject.getValueNormal();getValueNormal()은 myObject를 this로 받아 10을 출력하지만, getValueArrow()는 classic script에서 window.value를 읽어 undefined를 출력합니다. ES 모듈이라면 getValueArrow()에서 TypeError가 나 다음 호출에 이르지 못하므로, 각 호출을 따로 실행해 비교하세요. 호출한 객체를 this로 받아야 하는 메서드는 일반 함수나 메서드 단축 문법 method() { ... }로 정의합니다.
생성자 함수
화살표 함수에는 prototype 속성이 없고, new로 호출할 수 없습니다. new와 함께 호출하면 TypeError가 납니다.
// BAD: 화살표 함수는 생성자로 사용할 수 없음
// const MyClass = () => { this.value = 1; };
// const instance = new MyClass(); // TypeError: MyClass is not a constructor
// GOOD: 생성자는 일반 함수 또는 class 키워드로 정의
function MyClass(value) {
this.value = value;
}
const instance = new MyClass(10);arguments가 필요한 함수
일반 함수는 자신에게 전달된 인수를 arguments 객체로 읽을 수 있지만, 화살표 함수에는 자기 arguments가 없습니다. 화살표 함수 안에서 arguments를 쓰면 바깥 함수의 arguments를 읽고, 바깥에 함수가 없다면 ReferenceError가 납니다. 화살표 함수에서 인수를 모두 받으려면 나머지 매개변수(rest parameter) ...를 씁니다.
function sumAllNormal() {
console.log(arguments); // 1, 2, 3을 담은 arguments 객체 (배열은 아님)
return Array.from(arguments).reduce((total, value) => total + value, 0);
}
console.log(sumAllNormal(1, 2, 3)); // 6
// const brokenArrow = () => arguments; // 브라우저 최상위에서 호출하면 ReferenceError
// 화살표 함수는 나머지 매개변수로 인수를 배열로 받습니다.
const sumAllArrow = (...values) => values.reduce((total, value) => total + value, 0);
console.log(sumAllArrow(1, 2, 3)); // 6
console.log(sumAllArrow()); // 0arguments는 배열이 아니라서 Array.from()으로 배열로 바꾼 뒤 reduce()를 씁니다. 나머지 매개변수 values는 처음부터 진짜 배열이라 바로 배열 메서드를 쓸 수 있으므로, 일반 함수에서도 arguments보다 나머지 매개변수를 쓰는 편이 좋습니다. 두 함수 모두 reduce()에 초기값 0을 주었기 때문에 인수가 없을 때도 오류 없이 0을 돌려줍니다.
DOM 이벤트 리스너
일반 함수를 addEventListener()로 등록하면, 리스너 안의 this는 그 리스너가 등록된 요소인 event.currentTarget입니다. 이벤트가 버블링되는 중에는 이벤트가 처음 일어난 event.target과 다를 수 있습니다. 화살표 함수로 등록하면 this가 바깥의 this가 되므로, 요소를 this로 읽으려던 코드는 의도대로 동작하지 않습니다.
<button id="myButton">클릭하세요</button>
<script>
const button = document.getElementById('myButton');
// 일반 함수: this가 리스너를 등록한 버튼 요소
button.addEventListener('click', function() {
console.log("일반 함수 this:", this.id); // myButton
});
// 화살표 함수: this가 바깥 스코프의 this (window)
button.addEventListener('click', () => {
console.log("화살표 함수 this:", this.id); // undefined (window.id는 없음)
});
// 화살표 함수에서 버튼이 필요하면 event.currentTarget을 사용
button.addEventListener('click', (event) => {
console.log("currentTarget:", event.currentTarget.id); // myButton
});
</script>버튼을 클릭하면 세 리스너가 등록한 순서대로 실행됩니다. 화살표 함수에서도 리스너가 등록된 요소가 필요하다면 세 번째 리스너처럼 event.currentTarget을 쓰면 됩니다. event.currentTarget은 리스너가 실행되는 동안에만 그 요소를 가리키므로, 리스너 안에서 바로 읽어야 합니다.
더 깊이: 클래스 필드에 적은 화살표 함수
객체 리터럴의 메서드와 달리, class 본문에 handleClick = () => { ... };처럼 화살표 함수를 클래스 필드(class field)로 적으면 this가 인스턴스를 가리킵니다. 클래스 필드는 인스턴스를 만들 때 그 인스턴스를 this로 삼아 초기화되고, 화살표 함수는 그 this를 그대로 붙잡기 때문입니다. 그래서 button.addEventListener('click', counter.handleClick)처럼 메서드를 떼어 넘겨도 this가 바뀌지 않아, 이런 필드를 흔히 자동으로 바인딩된 메서드라고 부릅니다. 생성자에서 this.handleClick = this.handleClick.bind(this)를 쓰던 방식을 짧게 쓴 셈입니다.
대신 비용이 있습니다. 일반 메서드는 프로토타입에 하나만 있고 모든 인스턴스가 함께 쓰지만, 클래스 필드는 인스턴스마다 정의되므로 인스턴스를 만들 때마다 새 함수와 클로저가 하나씩 생깁니다. 인스턴스를 수천 개 만드는 클래스라면 메모리 사용량이 그만큼 늘어날 수 있습니다. 또 프로토타입에 있지 않으므로, 프로토타입의 메서드를 바꾸거나 Class.prototype.handleClick으로 메서드를 찾는 코드에서는 보이지 않습니다.
그래서 콜백으로 떼어 넘겨야 하는 메서드에만 화살표 필드를 쓰고, 나머지는 프로토타입의 일반 메서드로 두는 방법을 검토합니다. 동작과 비용은 MDN의 화살표 함수 설명에 정리되어 있습니다.
바깥 this를 유지한다는 뜻
화살표 함수가 바깥 this를 유지한다는 것은 그 객체를 기억한다는 뜻이지, 그 순간의 속성 값을 복사해 둔다는 뜻이 아닙니다. 다음 코드는 이 차이를 두 번의 출력으로 확인합니다. 첫 출력은 call()로 this를 바꿔 보려 하고, 두 번째 출력은 화살표 함수를 만든 뒤에 source.label을 바꾼 상태에서 읽습니다.
const source = {
label: "원본",
makeReader() {
return () => this.label;
}
};
const readLabel = source.makeReader();
console.log(readLabel.call({ label: "다른 객체" }));
source.label = "갱신";
console.log(readLabel());출력은 원본, 갱신입니다. 화살표 함수는 자신을 만든 source.makeReader() 호출의 this인 source를 기억합니다. 첫 줄에서 call()로 다른 객체를 넘겨도 화살표 함수의 this는 바뀌지 않아 source.label을 읽고, source.label을 바꾼 뒤에는 그 현재 값인 갱신을 읽습니다.
정리
- 화살표 함수는
=>로 함수를 짧게 쓰는 문법이며, 표현식 본문은 값을 바로 돌려주고 객체 리터럴은 소괄호로 감싸 돌려줍니다. - 화살표 함수는 자기
this를 만들지 않고 정의한 곳의 바깥this를 쓰며,call()이나bind()로도 바꿀 수 없습니다. - 그래서 메서드 안의 콜백처럼 바깥 메서드의
this를 유지해야 하는 곳에 잘 맞습니다. - 객체 리터럴에 화살표 함수로 적은 메서드는 객체를
this로 받지 못하므로, 메서드는 일반 함수나 메서드 단축 문법으로 정의합니다. - 화살표 함수는
new로 호출할 수 없고 자기arguments도 없으므로, 생성자는function이나class로, 가변 인수는 나머지 매개변수로 다룹니다. - 이벤트 리스너에서 요소가 필요할 때 화살표 함수를 쓴다면
this대신event.currentTarget을 읽습니다.
연습 문제
1. 일반 함수로 바꾸면
마지막 예제에서 makeReader()가 돌려주는 함수를 return function () { return this.label; };로 바꾸고 첫 console.log()만 실행합니다. 무엇이 출력될까요? 화살표 함수와 달리 call()의 인수가 영향을 주는 이유를 설명해 보세요.
해설 보기
다른 객체가 출력됩니다. 일반 함수는 호출할 때마다 this가 정해지므로, 이번 호출에서는 call()이 넘긴 { label: "다른 객체" }가 this가 됩니다.
화살표 함수는 만들어질 때의 바깥 this를 쓰기 때문에 call()의 인수를 무시했지만, 일반 함수는 그 인수를 this로 받습니다. 두 번째 readLabel()처럼 일반 함수를 그냥 호출하면 엄격 모드 여부에 따라 결과가 달라지므로, 이 문제는 첫 호출만 비교합니다.
2. 클래스 필드의 화살표 함수
다음 코드의 두 출력은 무엇일까요?
class Counter {
count = 0;
increment = () => {
this.count += 1;
};
}
const counterA = new Counter();
const inc = counterA.increment;
inc();
inc();
console.log(counterA.count);
const counterB = new Counter();
console.log(counterA.increment === counterB.increment);해설 보기
2와 false가 출력됩니다. 클래스 필드의 화살표 함수는 인스턴스를 만들 때 그 인스턴스를 this로 기억하므로, 메서드를 변수에 꺼내 따로 호출해도 this가 counterA로 유지되어 count가 두 번 늘어납니다.
대신 이 함수는 프로토타입에 하나만 있는 메서드가 아니라 인스턴스를 만들 때마다 새로 만들어지는 필드입니다. 그래서 counterA와 counterB의 increment는 서로 다른 함수 객체입니다. this를 유지하는 편리함과 인스턴스마다 함수가 생기는 비용을 함께 따져 고르면 됩니다.
3. 다른 객체로 옮긴 화살표 함수
다음 코드는 무엇을 출력할까요? other.f()처럼 other의 메서드로 호출했는데도 2가 아닌 이유는 무엇일까요?
const original = {
value: 1,
getArrow() {
return () => this.value;
}
};
const f = original.getArrow();
const other = { value: 2, f };
console.log(other.f());해설 보기
1이 출력됩니다. f는 original.getArrow()를 호출할 때 만들어진 화살표 함수라서, 그 호출의 this인 original을 기억합니다.
other.f()처럼 다른 객체의 메서드로 호출해도 화살표 함수는 호출 방식을 보지 않으므로 this가 other로 바뀌지 않습니다. getArrow가 화살표 함수가 아니라 function을 돌려줬다면 other.f()의 this는 other가 되어 2가 출력되었을 것입니다.
화살표 함수가 바깥 바인딩을 쓰는 규칙과 생성자로 쓸 수 없는 이유는 MDN의 화살표 함수 문서에서 확인할 수 있습니다.
다음 글 구조 분해 할당과 스프레드 연산자에서는 배열과 객체에서 필요한 값을 한 번에 꺼내는 구조 분해 할당과, 값을 펼치거나 모으는 ... 문법을 살펴봅니다.