문법적 설탕 (Syntactic Sugar)
혹시 이런 경험 있으신가요?
// ES6 클래스를 배우고 나서
class User {
constructor(name) {
this.name = name;
}
greet() {
return `Hello, ${this.name}`;
}
}
// "어? 이거 생성자 함수랑 뭐가 다르지?"
function User(name) {
this.name = name;
}
User.prototype.greet = function() {
return 'Hello, ' + this.name;
};
저도 처음 ES6 클래스를 배울 때 이런 의문이 들었습니다. “클래스가 생성자 함수보다 뭐가 더 좋은 걸까?”, “내부적으로는 어떻게 다를까?” 이것이 바로 문법적 설탕(Syntactic Sugar)의 핵심입니다. 겉모습은 다르지만 내부 동작은 동일하거나 매우 유사한 것이죠.
문법적 설탕은 마치 각설탕처럼, 코드에 “단맛”을 더해줍니다. 커피에 각설탕을 넣으나 가루 설탕을 넣으나 결국 단맛은 똑같습니다. 하지만 각설탕이 더 깔끔하고 사용하기 편하죠. 프로그래밍의 문법적 설탕도 마찬가지입니다.
왜 문법적 설탕을 이해해야 할까요?
1. 내부 동작 원리 파악
// ✨ 문법적 설탕: 화살표 함수
const double = x => x * 2;
// 🔍 실제로는 이렇게 동작
const double = function(x) {
return x * 2;
};
문법적 설탕을 이해하면 “겉모습”과 “실제 동작”을 구분할 수 있습니다. 이는 디버깅과 성능 최적화에 매우 중요합니다.
2. 레거시 코드 이해
많은 프로젝트에서 ES5 코드와 ES6+ 코드가 섞여 있습니다.
// 레거시 코드 (ES5)
var items = [1, 2, 3];
var doubled = items.map(function(item) {
return item * 2;
});
// 현대 코드 (ES6+)
const items = [1, 2, 3];
const doubled = items.map(item => item * 2);
문법적 설탕을 이해하면 이 두 코드가 본질적으로 같다는 것을 알 수 있습니다.
3. 올바른 도구 선택
문법적 설탕이라고 해서 항상 더 나은 것은 아닙니다.
// ❌ 화살표 함수는 this를 바인딩하지 않음
const button = {
text: 'Click me',
handleClick: () => {
console.log(this.text); // undefined!
}
};
// ✅ 일반 함수 사용
const button = {
text: 'Click me',
handleClick: function() {
console.log(this.text); // 'Click me'
}
};
내부 동작을 알면 언제 문법적 설탕을 사용하고, 언제 전통적인 방법을 사용해야 하는지 판단할 수 있습니다.
4. 트랜스파일러 이해
Babel 같은 트랜스파일러는 최신 문법을 구형 문법으로 변환합니다.
// 입력 (ES6)
class Animal {
constructor(name) {
this.name = name;
}
}
// Babel 출력 (ES5)
function Animal(name) {
this.name = name;
}
문법적 설탕을 이해하면 트랜스파일러가 무엇을 하는지 명확히 알 수 있습니다.
기본 개념: 문법적 설탕이란?
정의
문법적 설탕(Syntactic Sugar)은 프로그래밍 언어에서 읽기 쉽고 작성하기 편한 문법을 제공하면서도, 내부적으로는 기존의 기능을 사용하는 것을 말합니다.
핵심:
- 새로운 기능이 아님 - 기존 기능의 다른 표현
- 가독성 향상 - 코드를 더 읽기 쉽게 만듦
- 편의성 제공 - 코드를 더 빠르게 작성할 수 있게 함
- 동일한 결과 - 내부적으로는 같은 동작
일상 생활의 비유
커피를 주문할 때를 떠올려보세요:
❌ 전통적인 방법:
"에스프레소 2샷에 우유 200ml를 추가하고,
거품을 내서 위에 올려주세요."
✅ 문법적 설탕:
"카페라떼 톨 사이즈 주세요."
결과는 똑같지만, 두 번째가 훨씬 간단하고 명확합니다. 프로그래밍의 문법적 설탕도 이와 같습니다.
대표적인 예시들 (MDN 공식 문서 기반)
1. ES6 클래스 (Classes)
MDN 공식 정의
“Classes are in fact ‘special functions’, and just as you can define function expressions and function declarations, a class can be defined in two ways.”
“Classes in JS are built on prototypes but also have some syntax and semantics that are unique to classes.”
Before: 생성자 함수
// ES5 방식
function Animal(name, species) {
this.name = name;
this.species = species;
}
Animal.prototype.speak = function() {
return this.name + '가 소리를 냅니다.';
};
Animal.prototype.getInfo = function() {
return '종: ' + this.species + ', 이름: ' + this.name;
};
// 정적 메서드
Animal.createDog = function(name) {
return new Animal(name, '개');
};
const dog = new Animal('멍멍이', '개');
console.log(dog.speak()); // "멍멍이가 소리를 냅니다."
After: ES6 클래스
// ES6 클래스 (문법적 설탕)
class Animal {
constructor(name, species) {
this.name = name;
this.species = species;
}
speak() {
return `${this.name}가 소리를 냅니다.`;
}
getInfo() {
return `종: ${this.species}, 이름: ${this.name}`;
}
static createDog(name) {
return new Animal(name, '개');
}
}
const dog = new Animal('멍멍이', '개');
console.log(dog.speak()); // "멍멍이가 소리를 냅니다."
내부 동작 시각화
┌─────────────────────────────────────┐
│ class Animal { ... } │
│ (문법적 설탕) │
└──────────────┬──────────────────────┘
│
↓ 내부적으로는
┌─────────────────────────────────────┐
│ function Animal() { ... } │
│ Animal.prototype.speak = ... │
│ (전통적인 생성자 함수) │
└─────────────────────────────────────┘
클래스만의 고유한 특징
MDN에 따르면, 클래스는 단순한 문법적 설탕이 아니라 고유한 특징도 가지고 있습니다.
- Strict Mode 자동 적용
class MyClass { constructor() { // 자동으로 strict mode mistakes = 'oops'; // ReferenceError! } } - 호이스팅 방식 차이
// ✅ 함수 선언은 호이스팅됨 const instance1 = new MyFunction(); function MyFunction() {} // ❌ 클래스는 호이스팅 안됨 (let/const처럼) const instance2 = new MyClass(); // ReferenceError class MyClass {} - 메서드가 열거 불가능
class MyClass { method() {} } // 클래스 메서드는 enumerable: false console.log(Object.keys(MyClass.prototype)); // [] // 생성자 함수 메서드는 enumerable: true (기본값) function MyFunction() {} MyFunction.prototype.method = function() {}; console.log(Object.keys(MyFunction.prototype)); // ['method']
2. 화살표 함수 (Arrow Functions)
MDN 공식 정의
“Arrow function expressions are a compact alternative to a traditional function expression”
“Arrow functions don’t have their own bindings to
this”
Before: 일반 함수
// 일반 함수
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(function(num) {
return num * 2;
});
const sum = function(a, b) {
return a + b;
};
console.log(doubled); // [2, 4, 6, 8, 10]
console.log(sum(3, 5)); // 8
After: 화살표 함수
// 화살표 함수 (문법적 설탕)
const numbers = [1, 2, 3, 4, 5];
// 더 간결한 문법
const doubled = numbers.map(num => num * 2);
const sum = (a, b) => a + b;
console.log(doubled); // [2, 4, 6, 8, 10]
console.log(sum(3, 5)); // 8
문법 간소화 단계
// 1단계: 전통적인 함수
const greet1 = function(name) {
return 'Hello, ' + name;
};
// 2단계: 화살표 함수 기본형
const greet2 = (name) => {
return `Hello, ${name}`;
};
// 3단계: 중괄호 생략 (단일 표현식)
const greet3 = (name) => `Hello, ${name}`;
// 4단계: 괄호 생략 (매개변수 1개)
const greet4 = name => `Hello, ${name}`;
// 최종적으로 얼마나 간결해졌는지!
화살표 함수만의 고유한 특징
MDN에 따르면, 화살표 함수는 단순한 문법적 설탕이 아닙니다.
1. this 바인딩 차이
// 일반 함수: this는 호출 방식에 따라 결정
function Counter() {
this.count = 0;
setInterval(function() {
this.count++; // this가 window를 가리킴!
console.log(this.count); // NaN
}, 1000);
}
// 화살표 함수: lexical this (상위 스코프의 this)
function Counter() {
this.count = 0;
setInterval(() => {
this.count++; // this가 Counter 인스턴스를 가리킴
console.log(this.count); // 1, 2, 3, ...
}, 1000);
}
2. 생성자로 사용 불가
// ❌ 화살표 함수는 생성자로 사용 불가
const Person = (name) => {
this.name = name;
};
try {
const person = new Person('Alice'); // TypeError!
} catch (e) {
console.error(e.message); // "Person is not a constructor"
}
// ✅ 일반 함수는 생성자로 사용 가능
function Person(name) {
this.name = name;
}
const person = new Person('Alice'); // 동작
3. arguments 객체 없음
// 일반 함수: arguments 객체 사용 가능
function sum() {
return Array.from(arguments).reduce((a, b) => a + b, 0);
}
console.log(sum(1, 2, 3, 4)); // 10
// 화살표 함수: arguments 객체 없음
const sumArrow = () => {
console.log(arguments); // ReferenceError (strict mode)
};
// 대안: rest 파라미터 사용
const sumArrow2 = (...args) => {
return args.reduce((a, b) => a + b, 0);
};
console.log(sumArrow2(1, 2, 3, 4)); // 10
3. 템플릿 리터럴 (Template Literals)
MDN 공식 정의
“With template literals, you can avoid the concatenation operator — and improve the readability of your code — by using placeholders of the form
${expression}”
Before: 문자열 연결
// 문자열 연결 (+)
const name = 'Alice';
const age = 25;
const city = 'Seoul';
const message = 'My name is ' + name +
' and I am ' + age +
' years old. I live in ' + city + '.';
const multiline = 'This is line 1.\n' +
'This is line 2.\n' +
'This is line 3.';
const html = '<div class="user">\n' +
' <h2>' + name + '</h2>\n' +
' <p>Age: ' + age + '</p>\n' +
'</div>';
After: 템플릿 리터럴
// 템플릿 리터럴 (문법적 설탕)
const name = 'Alice';
const age = 25;
const city = 'Seoul';
// 문자열 보간 (String Interpolation)
const message = `My name is ${name} and I am ${age} years old. I live in ${city}.`;
// 멀티라인 문자열
const multiline = `This is line 1.
This is line 2.
This is line 3.`;
// HTML 템플릿
const html = `
<div class="user">
<h2>${name}</h2>
<p>Age: ${age}</p>
</div>
`;
표현식 사용
const a = 5;
const b = 10;
// ❌ 문자열 연결: 복잡하고 읽기 어려움
const oldWay = 'Fifteen is ' + (a + b) + ' and not ' + (2 * a + b) + '.';
// ✅ 템플릿 리터럴: 간결하고 명확
const newWay = `Fifteen is ${a + b} and not ${2 * a + b}.`;
console.log(oldWay); // "Fifteen is 15 and not 20."
console.log(newWay); // "Fifteen is 15 and not 20."
// 복잡한 표현식도 가능
const user = { name: 'Bob', scores: [85, 90, 92] };
const report = `
${user.name}의 성적:
- 평균: ${user.scores.reduce((a, b) => a + b, 0) / user.scores.length}점
- 최고점: ${Math.max(...user.scores)}점
- 최저점: ${Math.min(...user.scores)}점
`;
console.log(report);
Tagged Templates
템플릿 리터럴만의 고유한 기능:
// Tagged Template: 템플릿을 커스텀 처리
function highlight(strings, ...values) {
return strings.reduce((result, str, i) => {
const value = values[i] ? `<mark>${values[i]}</mark>` : '';
return result + str + value;
}, '');
}
const name = 'Alice';
const score = 95;
const result = highlight`학생 ${name}의 점수는 ${score}점입니다.`;
console.log(result);
// "학생 <mark>Alice</mark>의 점수는 <mark>95</mark>점입니다."
비교표: 문법적 설탕 vs 전통적 방법
| 기능 | 전통적 방법 | 문법적 설탕 | 내부 동작 |
|---|---|---|---|
| 클래스 | function User() {} |
class User {} |
프로토타입 기반 (+ strict mode, 호이스팅 차이) |
| 화살표 함수 | function(x) { return x * 2 } |
x => x * 2 |
일반 함수 (+ lexical this, 생성자 불가) |
| 템플릿 리터럴 | 'Hello, ' + name |
`Hello, ${name}` |
문자열 연결 (+ 멀티라인, tagged templates) |
| 비구조화 할당 | const x = obj.x |
const {x} = obj |
개별 할당 |
| for…of 루프 | for (var i = 0; i < arr.length; i++) |
for (const item of arr) |
Iterator 프로토콜 |
좋은 예 vs 나쁜 예
❌ 나쁜 예 1: 화살표 함수의 잘못된 사용
// ❌ 객체 메서드로 화살표 함수 사용 (this 문제)
const user = {
name: 'Alice',
greet: () => {
console.log(`Hello, ${this.name}`); // undefined!
}
};
user.greet(); // "Hello, undefined"
// ❌ 프로토타입 메서드로 화살표 함수 사용
function Person(name) {
this.name = name;
}
Person.prototype.greet = () => {
console.log(`Hello, ${this.name}`); // undefined!
};
const person = new Person('Bob');
person.greet(); // "Hello, undefined"
✅ 좋은 예 1: 화살표 함수의 올바른 사용
// ✅ 객체 메서드는 일반 함수 사용
const user = {
name: 'Alice',
greet() {
console.log(`Hello, ${this.name}`);
}
};
user.greet(); // "Hello, Alice"
// ✅ 콜백에서는 화살표 함수가 유용
class Timer {
constructor() {
this.seconds = 0;
}
start() {
// 화살표 함수로 this를 유지
setInterval(() => {
this.seconds++;
console.log(this.seconds);
}, 1000);
}
}
const timer = new Timer();
timer.start(); // 1, 2, 3, ...
❌ 나쁜 예 2: 과도한 문법적 설탕 중첩
// ❌ 너무 복잡한 템플릿 리터럴
const report = `
${users.map(user => `
<div>
${user.posts.map(post => `
<article>
${post.comments.map(comment => `
<p>${comment.text}</p>
`).join('')}
</article>
`).join('')}
</div>
`).join('')}
`;
// 읽기 어렵고 디버깅도 힘듦
✅ 좋은 예 2: 명확하고 간단하게
// ✅ 함수로 분리하여 명확하게
function renderComment(comment) {
return `<p>${comment.text}</p>`;
}
function renderPost(post) {
const comments = post.comments.map(renderComment).join('');
return `<article>${comments}</article>`;
}
function renderUser(user) {
const posts = user.posts.map(renderPost).join('');
return `<div>${posts}</div>`;
}
const report = users.map(renderUser).join('');
// 읽기 쉽고 각 함수를 테스트하기도 좋음
❌ 나쁜 예 3: 클래스를 굳이 사용
// ❌ 단순한 유틸리티에 클래스 사용
class MathUtils {
static add(a, b) {
return a + b;
}
static multiply(a, b) {
return a * b;
}
}
MathUtils.add(2, 3); // 5
// 인스턴스를 만들 필요가 없는데 클래스를 사용함
✅ 좋은 예 3: 단순한 객체나 함수 사용
// ✅ 간단한 객체 또는 개별 함수로 충분
const MathUtils = {
add: (a, b) => a + b,
multiply: (a, b) => a * b
};
// 또는
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
// 더 간단하고 명확함
함정과 주의사항
1. 화살표 함수의 this
// 함정: 화살표 함수는 자신만의 this를 가지지 않음
const obj = {
count: 0,
// ❌ 잘못된 방법
incrementWrong: () => {
this.count++; // this가 obj가 아님!
},
// ✅ 올바른 방법
incrementRight() {
this.count++;
}
};
obj.incrementWrong();
console.log(obj.count); // 0 (증가하지 않음)
obj.incrementRight();
console.log(obj.count); // 1 (정상 증가)
2. 클래스의 호이스팅
// 함정: 클래스는 함수와 달리 호이스팅되지 않음
// ✅ 함수는 호이스팅됨
const dog1 = new Dog1(); // 동작
function Dog1() {}
// ❌ 클래스는 호이스팅 안됨
try {
const dog2 = new Dog2(); // ReferenceError!
class Dog2 {}
} catch (e) {
console.error('클래스는 선언 전에 사용할 수 없습니다.');
}
// ✅ 클래스를 먼저 선언
class Dog2 {}
const dog2 = new Dog2(); // 동작
3. 템플릿 리터럴의 성능
// 주의: 복잡한 템플릿 리터럴은 성능에 영향
// ❌ 루프 안에서 복잡한 템플릿 리터럴
function generateHTML(items) {
let html = '';
for (let i = 0; i < items.length; i++) {
html += `
<div class="item">
<h3>${items[i].title}</h3>
<p>${items[i].description}</p>
<span>${new Date(items[i].date).toLocaleDateString()}</span>
</div>
`;
}
return html;
}
// ✅ 배열 메서드와 join 사용
function generateHTMLBetter(items) {
return items.map(item => `
<div class="item">
<h3>${item.title}</h3>
<p>${item.description}</p>
<span>${new Date(item.date).toLocaleDateString()}</span>
</div>
`).join('');
}
4. 비구조화 할당의 혼란
// 함정: 기본값과 재할당 혼동
const user = {
name: 'Alice',
age: 25
};
// ❌ 혼란스러운 비구조화
const { name = 'Unknown', age, email = 'no-email' } = user;
console.log(email); // 'no-email' (기본값)
// ✅ 명확한 비구조화
const {
name = 'Unknown',
age,
email = 'no-email'
} = user || {};
// 더 명확하게: 주석 추가
const {
name = 'Unknown', // user.name이 없으면 'Unknown'
age, // user.age
email = 'no-email' // user.email이 없으면 'no-email'
} = user;
실전 활용
1. React 컴포넌트에서의 활용
// 전통적인 방법
class UserProfile extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState({ count: this.state.count + 1 });
}
render() {
return React.createElement('div', null,
React.createElement('h1', null, 'User: ' + this.props.name),
React.createElement('p', null, 'Count: ' + this.state.count),
React.createElement('button', { onClick: this.handleClick }, 'Increment')
);
}
}
// 문법적 설탕 활용
class UserProfile extends React.Component {
state = { count: 0 };
// 클래스 필드 + 화살표 함수 (자동 바인딩)
handleClick = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
// JSX + 템플릿 리터럴
return (
<div>
<h1>User: {this.props.name}</h1>
<p>Count: {this.state.count}</p>
<button onClick={this.handleClick}>Increment</button>
</div>
);
}
}
2. 배열 메서드 체이닝
// 전통적인 방법
const users = [
{ name: 'Alice', age: 25, active: true },
{ name: 'Bob', age: 30, active: false },
{ name: 'Charlie', age: 35, active: true }
];
// ❌ 전통적인 for 루프
const activeUserNames = [];
for (let i = 0; i < users.length; i++) {
if (users[i].active) {
activeUserNames.push(users[i].name.toUpperCase());
}
}
// ✅ 화살표 함수 + 메서드 체이닝
const activeUserNamesNew = users
.filter(user => user.active)
.map(user => user.name.toUpperCase());
console.log(activeUserNamesNew); // ['ALICE', 'CHARLIE']
3. Promise 체이닝
// 전통적인 방법
fetch('/api/user/1')
.then(function(response) {
return response.json();
})
.then(function(user) {
return fetch('/api/posts?userId=' + user.id);
})
.then(function(response) {
return response.json();
})
.then(function(posts) {
console.log('사용자의 게시글:', posts);
})
.catch(function(error) {
console.error('에러 발생:', error);
});
// 문법적 설탕 활용 (화살표 함수 + 템플릿 리터럴)
fetch('/api/user/1')
.then(response => response.json())
.then(user => fetch(`/api/posts?userId=${user.id}`))
.then(response => response.json())
.then(posts => console.log('사용자의 게시글:', posts))
.catch(error => console.error('에러 발생:', error));
// async/await로 더 간결하게 (더 높은 수준의 문법적 설탕)
async function getUserPosts(userId) {
try {
const userResponse = await fetch(`/api/user/${userId}`);
const user = await userResponse.json();
const postsResponse = await fetch(`/api/posts?userId=${user.id}`);
const posts = await postsResponse.json();
console.log('사용자의 게시글:', posts);
} catch (error) {
console.error('에러 발생:', error);
}
}
4. 객체 리터럴 확장
// 전통적인 방법
const name = 'Alice';
const age = 25;
const user = {
name: name,
age: age,
greet: function() {
console.log('Hello, ' + this.name);
}
};
// 문법적 설탕 활용
const userNew = {
name, // 축약 프로퍼티
age,
greet() { // 축약 메서드
console.log(`Hello, ${this.name}`);
},
// 계산된 프로퍼티 이름
[`user_${Date.now()}`]: true
};
트랜스파일러가 하는 일
Babel 같은 트랜스파일러는 최신 문법적 설탕을 구형 브라우저가 이해할 수 있는 코드로 변환합니다.
클래스 변환 예시
// 입력 (ES6)
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name}가 소리를 냅니다.`;
}
}
// Babel 출력 (ES5)
"use strict";
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
var Animal = function () {
function Animal(name) {
_classCallCheck(this, Animal);
this.name = name;
}
Animal.prototype.speak = function speak() {
return this.name + "\uAC00 \uC18C\uB9AC\uB97C \uB0C5\uB2C8\uB2E4.";
};
return Animal;
}();
화살표 함수 변환 예시
// 입력 (ES6)
const numbers = [1, 2, 3];
const doubled = numbers.map(x => x * 2);
// Babel 출력 (ES5)
"use strict";
var numbers = [1, 2, 3];
var doubled = numbers.map(function (x) {
return x * 2;
});
언제 사용하고 언제 피해야 할까?
✅ 사용해야 할 때
- 가독성이 크게 향상될 때
// ✅ 훨씬 읽기 쉬움 const greeting = `Hello, ${name}!`; // ❌ 복잡함 const greeting = 'Hello, ' + name + '!'; - 팀의 코딩 스타일에 맞을 때
// 팀이 ES6+를 사용한다면 const users = data.map(item => new User(item)); - 기능적 이점이 있을 때
// ✅ 화살표 함수의 lexical this setTimeout(() => { this.handleUpdate(); }, 1000);
❌ 피해야 할 때
- 내부 동작을 이해하지 못할 때
// 화살표 함수의 this를 이해하지 못한다면 사용 금지 - 브라우저 호환성이 중요할 때
// IE11 지원이 필요하고 트랜스파일러가 없다면 // ES5 문법 사용 - 성능이 중요할 때
// 초고성능이 필요한 루프에서는 // 전통적인 for 루프가 더 빠를 수 있음 for (let i = 0; i < 1000000; i++) { // ... }
참고 자료
MDN 공식 문서
-
[Classes - JavaScript MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes) -
[Arrow Functions - JavaScript MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions) -
[Template Literals - JavaScript MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) -
[Destructuring Assignment - JavaScript MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) -
[for…of - JavaScript MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for…of)
ECMAScript 명세
추가 학습
- JavaScript.info - Modern JavaScript
- Babel REPL - 트랜스파일 결과 확인
- You Don’t Know JS - Kyle Simpson
댓글