ES6는 무엇을 바꿨을까: 최신 문법보다 중요한 자바스크립트 실행 의미 이해하기
빠른 답
- ES6는 현재의 최신 버전이 아니라 2015년에 표준화된 ECMAScript 2015를 가리키는 중요한 전환점입니다.
let과const는 단순한var대체 문법이 아니라 블록 스코프와 TDZ 때문에 값 대신 오류가 관찰될 수 있습니다.- 화살표 함수는 짧은 함수 문법보다 자신만의
this를 만들지 않는다는 차이가 더 중요합니다. - ES6 이전 문법은 레거시 코드, 트랜스파일 결과, 오래된 예제 해석을 위해 여전히 알아둘 필요가 있습니다.
목차
한눈에 비교
ES6는 최신 버전이 아니라 ECMAScript 2015라는 기준점이다
ES6라는 이름은 지금도 널리 쓰이지만, 현재 기준으로 “자바스크립트 최신 문법”을 뜻하지는 않습니다. ES6는 ECMAScript 2015를 가리키는 이름에 가깝습니다. 이 버전에서 let, const, 화살표 함수, 클래스, 모듈, 구조 분해 할당, 스프레드 문법, Promise 같은 기능이 한꺼번에 들어오면서 자바스크립트 코드의 형태와 읽는 방식이 크게 달라졌습니다.
버전 표현을 분명히 해 두는 이유는 오래된 글에서 “ES6 최신 문법”이라는 표현이 자주 보이기 때문입니다. 2026년 4월 기준 공식 연간 스냅샷은 ECMAScript 2025 Language Specification이고, TC39의 ECMAScript 2026 초안은 다음 표준을 향해 갱신되는 편집본입니다. ES6를 “최신”으로 외우기보다 “현대 자바스크립트의 큰 기준점”으로 보는 편이 현재 코드를 읽을 때 덜 헷갈립니다.
ES6 이전 문법이 사라진 것도 아닙니다. var, 함수 선언식, 생성자 함수, 프로토타입 상속, 콜백 패턴은 여전히 많은 코드에 남아 있습니다. 라이브러리 내부 코드, 번들러가 변환한 결과물, 오래된 브라우저 대응 코드에서는 ES6 이전 스타일을 자주 만나게 됩니다.
스코프와 선언 방식이 바꾸는 관찰 가능한 결과
let과 const를 “요즘 쓰는 변수 선언법” 정도로만 보면 중요한 차이를 놓치기 쉽습니다. 실제 차이는 스코프, 초기화 시점, 접근했을 때 발생하는 값 또는 오류에서 나타납니다.
var는 함수 스코프를 갖고, 선언 전에 접근하면 undefined로 관찰될 수 있습니다. 반면 let과 const는 블록 스코프를 가지며, 선언문에 도달하기 전까지 TDZ에 놓입니다. TDZ는 Temporal Dead Zone의 약자로, 이름은 스코프에 등록되어 있지만 아직 접근할 수 없는 구간을 뜻합니다.
console.log(oldName); // undefined
var oldName = "var";
console.log(newName); // ReferenceError
let newName = "let";
두 번째 예시의 ReferenceError는 “호이스팅이 안 된다”는 뜻으로만 보면 부족합니다. let 선언도 스코프에 등록되지만 초기화되기 전 접근이 금지됩니다. 이 차이 때문에 typeof도 항상 안전한 검사 도구가 되지는 않습니다.
console.log(typeof maybeGlobal); // "undefined"
console.log(typeof title); // ReferenceError
let title = "ES2015";
const도 자주 오해됩니다. const는 객체를 얼리는 문법이 아니라 바인딩 재할당을 막는 문법입니다. 객체 내부 상태는 여전히 바뀔 수 있습니다.
const user = { name: "Kim" };
user.name = "Lee";
console.log(user.name); // "Lee"
user = { name: "Park" }; // TypeError
여기서 user라는 이름이 다른 객체를 가리키도록 바꾸는 일은 재할당입니다. 반면 user.name을 바꾸는 일은 객체 내부 상태 변경입니다. const가 막는 것은 재할당이지, 참조된 객체의 모든 변경이 아닙니다.
this, 클래스, 모듈은 문법보다 실행 의미가 중요하다
화살표 함수는 짧게 쓰는 함수 문법으로 소개되곤 하지만, 실행 의미에서 더 큰 차이는 this입니다. 일반 함수의 this는 함수가 어떻게 호출되었는지에 따라 정해집니다. 화살표 함수는 자신만의 this를 만들지 않고 바깥 스코프의 this를 사용합니다.
const counter = {
count: 0,
normal() {
setTimeout(function () {
console.log(this.count);
}, 0);
},
arrow() {
setTimeout(() => {
console.log(this.count);
}, 0);
},
};
counter.normal(); // undefined 또는 런타임 문맥에 따른 값
counter.arrow(); // 0
normal 안의 일반 함수는 counter의 메서드로 호출된 것이 아닙니다. 그래서 그 안의 this가 counter를 가리킨다고 보기 어렵습니다. 반면 arrow 안의 화살표 함수는 arrow 메서드가 실행될 때의 this를 그대로 사용하므로 counter.count에 접근합니다.
class 문법도 자바스크립트에 Java나 C# 같은 클래스 모델이 새로 생겼다는 뜻은 아닙니다. ES6의 class는 프로토타입 기반 상속을 더 정돈된 형태로 표현합니다. 생성자는 constructor로 쓰고, 메서드는 프로토타입에 올라갑니다. 객체의 속성 조회, 메서드 공유, 상속 체인은 여전히 프로토타입 의미론 위에서 동작합니다.
모듈 문법 역시 단순한 파일 분리 문법이 아닙니다. import와 export는 파일 간 의존성을 정적으로 드러냅니다. 또한 ECMAScript 모듈은 기본적으로 엄격 모드로 평가되고, 가져온 바인딩은 단순 복사본이 아니라 라이브 바인딩으로 동작합니다. 이 특성은 MDN import 문서에서도 확인할 수 있습니다.
구조 분해와 스프레드에서 값과 참조 구분하기
구조 분해 할당은 객체나 배열에서 값을 꺼내는 문법입니다. 다만 기본값이 적용되는 조건을 정확히 알아야 합니다. 기본값은 값이 undefined일 때 적용되고, null일 때는 적용되지 않습니다.
const config = {
retry: undefined,
timeout: null,
};
const { retry = 3, timeout = 1000 } = config;
console.log(retry); // 3
console.log(timeout); // null
이 차이는 설정 병합 코드에서 자주 드러납니다. undefined는 값이 제공되지 않았다는 의미로 쓰이는 경우가 많고, null은 일부러 비워 둔 값이라는 의미로 쓰일 수 있습니다. 구조 분해 기본값은 이 정책을 대신 판단하지 않고, undefined에만 반응합니다.
스프레드 문법도 복사처럼 보이지만 깊은 복사는 아닙니다. 바깥 객체는 새로 만들어지지만, 내부 객체 참조는 공유됩니다.
const original = {
name: "post",
meta: { views: 0 },
};
const copied = { ...original };
copied.meta.views = 10;
console.log(original.meta.views); // 10
이 결과는 스프레드가 잘못된 문법이라는 뜻이 아닙니다. 스프레드는 얕은 복사를 수행합니다. 중첩 상태를 독립적으로 다루려면 중첩 객체도 함께 새로 만들거나, 실행 환경에서 제공하는 구조화 복사 기능을 검토해야 합니다.
Promise는 값과 오류의 흐름을 함께 다룬다
Promise는 비동기 작업을 “나중에 값이 생긴다” 정도로만 이해하면 오류 전파를 놓치기 쉽습니다. Promise는 대기, 이행, 거부 상태를 가지며, then과 catch는 그 상태 전환을 이어 붙이는 방식입니다.
function fetchUserName(id) {
return Promise.resolve({ id, name: "Kim" })
.then((user) => user.name)
.then((name) => name.toUpperCase());
}
fetchUserName(1).then(console.log); // "KIM"
오류가 발생하면 다음 then의 성공 경로로 계속 내려가지 않고, 가장 가까운 거부 처리로 이동합니다.
Promise.resolve("42")
.then((value) => {
throw new Error(`invalid id: ${value}`);
})
.then(() => {
console.log("not reached");
})
.catch((error) => {
console.log(error.message); // "invalid id: 42"
});
콜백 코드에서는 오류를 인자로 넘길지, 예외로 던질지, 어디에서 처리할지 규칙이 흩어지기 쉽습니다. Promise는 비동기 결과와 오류를 하나의 체인에서 읽게 해 줍니다. 이후 ES2017에 추가된 async와 await도 겉으로는 동기 코드처럼 보이지만, 바탕에는 Promise의 상태와 오류 전파 모델이 있습니다. 따라서 async와 await를 ES6 기능으로 묶어 설명하는 오래된 글은 현재 기준으로는 구분해서 읽는 편이 좋습니다.
Babel과 호환성 설정에서 나누어 봐야 할 것
ES6 문법을 작성한다고 해서 모든 실행 환경이 그대로 이해하는 것은 아닙니다. 최신 브라우저나 최신 Node.js만 대상으로 한다면 많은 ES6 기능을 변환 없이 사용할 수 있습니다. 반대로 오래된 브라우저, 구형 웹뷰, 특정 임베디드 런타임을 지원해야 한다면 문법 변환과 폴리필을 따로 봐야 합니다.
Babel의 @babel/preset-env는 대상 환경에 맞춰 필요한 변환과 폴리필 주입을 조정합니다. 공식 Babel preset-env 문서는 브라우저 타깃을 Browserslist 설정으로 관리하는 방식을 안내합니다.
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": "3.37"
}
]
]
}
브라우저 범위는 별도 설정 파일로 분리하면 빌드 도구와 CSS 도구가 같은 기준을 공유하기 쉽습니다.
> 0.5%
last 2 versions
not dead
여기서 중요한 구분은 문법 변환과 런타임 기능 보완이 같지 않다는 점입니다. let, const, 화살표 함수, 클래스 문법은 Babel이 오래된 문법 형태로 바꿀 수 있습니다. 반면 Promise, Map, Set, 일부 배열 메서드 같은 내장 API는 실행 환경에 없으면 폴리필이 필요합니다.
마이그레이션 관점에서는 @babel/polyfill을 그대로 쓰는 오래된 설정도 주의해야 합니다. Babel 7.4.0 이후 @babel/polyfill 방식은 더 이상 권장되지 않고, core-js를 직접 추가한 뒤 @babel/preset-env의 useBuiltIns와 corejs 옵션으로 관리하는 방향이 일반적입니다. 또한 corejs 값은 프로젝트에 설치한 core-js의 실제 버전과 맞춰야 합니다.
흔한 오해와 현재 기준의 해석
ES6를 배운 뒤 흔히 생기는 오해 중 하나는 class를 쓰면 자바스크립트가 클래스 기반 언어로 바뀐다고 생각하는 것입니다. 그러나 자바스크립트 객체는 여전히 프로토타입 체인을 통해 속성을 찾습니다. class는 코드를 더 정돈해서 쓰게 해 주지만, 객체 모델 자체를 다른 언어처럼 바꾸지는 않습니다.
const에 대한 오해도 많습니다. const는 불변 데이터를 만드는 문법이 아닙니다. 바인딩 재할당을 막을 뿐입니다. 객체 내부 속성 변경까지 막으려면 Object.freeze 같은 별도 조치가 필요하고, 그 역시 기본적으로 얕은 동결입니다. 상태 변경을 추적해야 하는 코드라면 const 사용 여부보다 참조 공유 여부를 함께 봐야 합니다.
화살표 함수도 모든 일반 함수를 대체하지 않습니다. 객체 메서드 자체를 화살표 함수로 만들면 그 함수의 this는 객체를 가리키지 않을 수 있습니다. 생성자 함수로 사용할 수도 없습니다. 짧게 쓸 수 있다는 장점보다 this, arguments, 생성자 사용 가능 여부가 달라진다는 점을 먼저 확인해야 합니다.
모듈 문법은 실행 환경의 로딩 규칙과 함께 이해해야 합니다. 브라우저, Node.js, 번들러는 모듈 해석 방식, 파일 확장자, 패키지 설정을 다르게 다룰 수 있습니다. 문법은 ECMAScript 표준에 속하지만, 파일을 어떻게 찾고 실행할지는 런타임과 도구의 책임이 섞여 있습니다.
ES6 이전 문법을 여전히 알아야 하는 이유
ES6 이전 문법은 과거 지식이 아니라 현재 코드 읽기의 일부입니다. 빌드 결과물을 보면 class가 생성자 함수와 프로토타입 할당으로 바뀌어 있을 수 있습니다. 화살표 함수는 this를 보존하기 위해 바깥의 this를 변수에 담는 형태로 변환되기도 합니다. 이런 결과물을 읽으려면 함수 스코프, 생성자 함수, 프로토타입 의미를 알아야 합니다.
오래된 예제나 Stack Overflow 답변도 var, 즉시 실행 함수, 콜백 패턴을 기반으로 설명하는 경우가 많습니다. 이런 코드를 새 문법으로 바꿀 때는 겉모양보다 보존해야 할 동작을 먼저 확인해야 합니다. 특히 this와 비동기 오류 처리는 문법만 바꾸면 관찰 가능한 결과가 달라질 수 있습니다.
ES6를 공부한다는 것은 새 문법 이름을 외우는 일에 그치지 않습니다. 선언이 언제 초기화되는지, 값과 참조가 어떻게 공유되는지, 오류가 어떤 경로로 전달되는지, 함수가 어떤 this를 갖는지를 읽는 일에 가깝습니다. 이 기준이 잡히면 이후에 추가된 문법도 “더 최신이라서 좋다”가 아니라 “어떤 실행 의미를 더 분명하게 표현하는가”라는 관점으로 이해할 수 있습니다.
원문 참고
https://www.maeil-mail.kr/question/67
댓글
댓글 쓰기