[바미] ES2015에 대하여
들어가며...
2015년부터 자바스크립트에 매우 커다란 변화가 있었는데 이를 ES2015 또는 ES6라고 부릅니다.
이 시기를 기점으로 자바스크립트는 매년 새로운 문법에 대해 발표가 진행되고 있습니다.
(참고로 2024년 현재 ES2024까지 나와있습니다.)
그럼 이제 어떤 부분이 추가가 되었는 지 확인해봅시다!
변경점
1. let과 const
자바스크립트를 처음 배우는 분들이라면 var를 사용하여 변수를 선언하시는 방법을 배우셨을 겁니다.
이제 var대신 let과 const를 쓰셔야 할 때입니다.
아래 예시로 차이점을 보시죠.
function scopeTest() {
if (true) {
var varVariable = "I am a var variable";
let letVariable = "I am a let variable";
const constVariable = "I am a const variable";
}
console.log(varVariable); // 정상 출력: "I am a var variable"
console.log(letVariable); // ReferenceError: letVariable is not defined
console.log(constVariable); // ReferenceError: constVariable is not defined
}
scopeTest();
여기서 간략하게 var, let, const에 대한 차이점을 설명하자면
우선 var는 함수 스코프를 가지고, 중복 선언이 가능하고, 호이스팅이 발생하는 키워드이고,
let은 블록 스코프를 가지고, 중복 선언이 불가능하고 호이스팅은 되지만 초기화 전에 접근할 수 없는 키워드이고,
const는 let과 유사하지만, 한 번 값을 할당하면 재할당이 불가능한 키워드입니다. 호이스팅이 무엇인지는 아래 링크 참고 바랍니다!
보통은 let 키워드를 사용하여 변수를 생성하고, 상수 값의 성격을 가지는 변수는 const를 많이 사용합니다.
2. 템플릿 문자열
템플릿 문자열 역시 ES2015 문법에 추가된 문법입니다.
기존에 ' '나 " "로 감싸는 문자열과 다르게 ` `(백틱)으로 감싸며 `${}`구문을 사용하여 문자열 안에 변수를 직접 삽입하는 문법입니다.
템플릿 문자열 나오기 전
var name = "Alice";
var age = 25;
var job = "developer";
var introduction = "My name is " + name + ". I am " + age + " years old, and I work as a " + job + ".";
console.log(introduction);
// 출력: My name is Alice. I am 25 years old, and I work as a developer.
// 여러 줄 문자열 예제
var multilineString = "This is a multiline string.\nIt spans multiple lines.\nNo need for special characters to break lines.";
console.log(multilineString);
/*
출력:
This is a multiline string.
It spans multiple lines.
No need for special characters to break lines.
*/
템플릿 문자열 변경 후
const name = "Alice";
const age = 25;
const job = "developer";
// 템플릿 문자열 사용 예제
const introduction = `My name is ${name}. I am ${age} years old, and I work as a ${job}.`;
console.log(introduction);
// 출력: My name is Alice. I am 25 years old, and I work as a developer.
// 여러 줄 문자열 예제
const multilineString = `
This is a multiline string.
It spans multiple lines.
No need for special characters to break lines.
`;
console.log(multilineString);
/*
출력:
This is a multiline string.
It spans multiple lines.
No need for special characters to break lines.
*/
템플릿 문자열을 사용하지 않은 예제와 비교했을 때보다 훨씬 깔끔해진 모습을 확인하실 수 있습니다.
3. 객체 리터럴
객체 리터럴은 JavaScript에서 객체를 쉽게 생성하는 방법 중 하나입니다. 이 것도 ES2015에 추가됐죠.
객체는 키-값 쌍으로 구성되며, 객체 리터럴을 사용하면 중괄호 {} 안에 키와 값을 정의하여 객체를 직접 만들 수 있는데
객체 리터럴은구조가 간단하고, 간결하다보니 자주 사용합니다.
객체 리터럴 사용 전
// Object 생성자 함수를 사용하여 객체 생성
var person = new Object();
// 객체의 속성 추가
person.name = "Alice";
person.age = 25;
person.job = "developer";
// 메서드 추가
person.greet = function() {
console.log("Hello, my name is " + this.name);
};
// 객체의 속성에 접근
console.log(person.name); // 출력: Alice
console.log(person.age); // 출력: 25
console.log(person.job); // 출력: developer
// 메서드 호출
person.greet(); // 출력: Hello, my name is Alice
객체 리터럴 사용 후
const name = "Alice";
const age = 25;
const job = "developer";
// 객체 리터럴을 사용한 예
const person = {
name, // name: name
age, // age: age
job, // job: job
greet() {
console.log(`Hello, my name is ${this.name}`);
}
};
// 객체의 속성에 접근
console.log(person.name); // 출력: Alice
console.log(person.age); // 출력: 25
console.log(person.job); // 출력: developer
// 메서드 호출
person.greet(); // 출력: Hello, my name is Alice
greet()같은 객체의 메서드에 함수를 연결할 때 :(콜론)과 function을 사용하지 않아도 되고, 객체 리터럴에서 속성명과 변수명이 동일한 경우, 해당 변수명을 한 번만 작성해도 되는데 이를 단축 속성명 (Shorthand property names)이라고 부릅니다.
4. 화살표 함수
화살표 함수는 ES6(ECMAScript 2015)에서 도입된 새로운 함수 표현식으로, 기존의 함수 선언 방식보다 더 간결해졌습니다.
예시를 통해 알아보죠!
화살표 함수 사용 했을 때
// 화살표 함수 사용
const add = (a, b) => a + b;
console.log(add(2, 3)); // 출력: 5
// 화살표 함수를 사용한 콜백 예제
const numbers = [1, 2, 3, 4];
const squares = numbers.map(n => n * n);
console.log(squares); // 출력: [1, 4, 9, 16]
화살표 함수를 사용하지 않았을 때
// 전통적인 함수 표현식 사용
const add = function(a, b) {
return a + b;
};
console.log(add(2, 3)); // 출력: 5
// 전통적인 함수 표현식을 사용한 콜백 예제
const numbers = [1, 2, 3, 4];
const squares = numbers.map(function(n) {
return n * n;
});
console.log(squares); // 출력: [1, 4, 9, 16]
문법이 조금 더 간결해 진거 보이시나요? 물론 무조건 문법이 간결해졌다고 좋은 것이 아니기 때문에 가독성도 고려해서 쓰셔야 합니다.
추가로 화살표 함수는 this 바인딩 방식이 기존 함수와 다르기 때문에, 콜백 함수나 메서드 내부에서 this를 다루기 더 쉽게 만들어주는데 예시를 통해 알아봅시다.
화살표 함수를 사용한 this 바인딩 예
const person = {
name: "Alice",
age: 25,
greet: function() {
console.log("Hello, my name is " + this.name);
},
greetLater: function() {
setTimeout(() => {
console.log("Hello, my name is " + this.name); // `this`는 여전히 `person`을 가리킴
}, 1000);
}
};
person.greet(); // 출력: Hello, my name is Alice
person.greetLater(); // 출력: Hello, my name is Alice
화살표 함수를 사용하지 않은 this 바인딩 예
const person = {
name: "Alice",
age: 25,
greet: function() {
console.log("Hello, my name is " + this.name);
},
greetLater: function() {
setTimeout(function() {
console.log("Hello, my name is " + this.name); // `this`가 다르게 바인딩됨
}, 1000);
}
};
person.greet(); // 출력: Hello, my name is Alice
person.greetLater(); // 출력: Hello, my name is undefined
보시는 것 처럼 화살표 함수는 코드의 간결함과 this 바인딩에서 매우 유용합니다. 특히 콜백 함수나 비동기 코드에서 this를 다루기 쉽게 만들어 주기 때문에 상황에 맞게 적절히 사용하시는 걸 추천드립니다.
5. 구조분해 할당
구조분해 할당은 ES6에서 도입된 문법으로, 배열이나 객체의 값을 간편하게 추출하여 변수에 할당할 수 있는 방법을 의미합니다.
아래 예를 통해 배열과 객체에서 구조분해 할당을 사용했을 때 어떤 차이가 있는 지 확인해보시죠!
구조분해 할당을 사용한 경우(배열)
const numbers = [1, 2, 3];
const [first, second, third] = numbers;
console.log(first); // 출력: 1
console.log(second); // 출력: 2
console.log(third); // 출력: 3
구조분해 할당을 사용하지 않은 경우(배열)
const numbers = [1, 2, 3];
const first = numbers[0];
const second = numbers[1];
const third = numbers[2];
console.log(first); // 출력: 1
console.log(second); // 출력: 2
console.log(third); // 출력: 3
구조분해 할당을 사용한 경우(객체)
const person = { name: "Alice", age: 25, job: "developer" };
const { name, age, job } = person;
console.log(name); // 출력: Alice
console.log(age); // 출력: 25
console.log(job); // 출력: developer
구조분해 할당을 사용하지 않은 경우(객체)
const person = { name: "Alice", age: 25, job: "developer" };
const name = person.name;
const age = person.age;
const job = person.job;
console.log(name); // 출력: Alice
console.log(age); // 출력: 25
console.log(job); // 출력: developer
먼저 배열부분부터 살펴보자면 구조분해 할당을 사용하지 않으면 각 요소를 개별적으로 인덱스를 통해 접근해야 했지만
구조분해 할당을 사용하여 배열의 각 요소를 직접 변수로 추출할 수 있게 되었습니다.
변수 선언과 동시에 값을 할당할 수 있기 때문에 코드가 간결해졌습니다.
그 다음 객체부분을 살펴볼까요?
구조분해 할당을 사용하지 않았을 때 예를 보면 객체의 각 속성에 개별적으로 접근하여 변수를 선언하고 할당해야 했었습니다. 하지만 구조분해 할당을 사용하여 객체의 속성을 비교적 쉽게 추출하여 변수로 할당할 수 있습니다.
참고로 이 과정에서 변수명은 객체의 키와 동일하게 설정됩니다.
6. 클래스
클래스는 객체 지향 프로그래밍의 주요 개념 중 하나이면서 JavaScript에서는 ES6에서 도입된 개념입니다.
생성자, 메서드, 상속이라는 개념이 추가가 되었습니다.
물론 Java와 같은 클래스 기반으로 동작하는 것이 아니라 프로토타입 기반으로 동작하며, 프로토타입 기반의 문법을 클래스로 바꾼 겁니다.
프로토타입(Prototype)❓
프로토타입(Prototype)은 JavaScript에서 객체 지향 프로그래밍을 구현하는 중요한 개념인데 모든 JavaScript 객체는 다른 객체로부터 상속을 받을 수 있는데 이 상속의 메커니즘이 바로 '프로토타입'입니다.
객체는 생성될 때 내부적으로 [[Prototype]]이라는 숨겨진 링크를 가지는데, 이것이 그 객체의 프로토타입을 참조하게 되는 것이죠.
클래스를 사용한 예
// 부모 클래스 (상위 클래스)
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
}
// 자식 클래스 (하위 클래스)
class Employee extends Person {
constructor(name, age, job) {
super(name, age); // 부모 클래스의 생성자를 호출하여 name과 age를 초기화
this.job = job;
}
// Employee 클래스에만 있는 메서드
work() {
console.log(`${this.name} is working as a ${this.job}.`);
}
}
// 클래스 인스턴스 생성
const employee = new Employee("Alice", 25, "developer");
employee.greet(); // 출력: Hello, my name is Alice.
employee.work(); // 출력: Alice is working as a developer.
클래스를 사용하지 않은 예
// 부모 생성자 함수
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.greet = function() {
console.log(`Hello, my name is ${this.name}.`);
};
// 자식 생성자 함수
function Employee(name, age, job) {
Person.call(this, name, age); // 부모 생성자 함수를 호출하여 속성 초기화
this.job = job;
}
// 상속을 설정
Employee.prototype = Object.create(Person.prototype);
Employee.prototype.constructor = Employee;
// 자식 클래스에만 있는 메서드
Employee.prototype.work = function() {
console.log(`${this.name} is working as a ${this.job}.`);
};
// 인스턴스 생성 및 메서드 호출
const employee = new Employee("Alice", 25, "developer");
employee.greet(); // 출력: Hello, my name is Alice.
employee.work(); // 출력: Alice is working as a developer.
참고로 클래스를 사용하지 않는 전통적인 방식도 여전히 사용하는 곳이 많습니다.
7. 프로미스(Promise)
콜백 지옥(callback hell)을 방지하기 위해 만들어진 객체인데 주로 JavaScript에서 비동기 작업을 처리하기 위해 사용됩니다.
또한 미래에 완료될 작업을 나타내며, 작업의 성공 또는 실패에 따라 다른 처리를 할 수 있게 해줍니다.
프로미스의 상태
대기(pending) - 초기 상태, 작업이 아직 완료되지 않은 상태.
이행(fulfilled) - 작업이 성공적으로 완료된 상태.
거부(rejected) - 작업이 실패한 상태.
프로미스를 사용한 예제
// 프로미스를 반환하는 함수
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const success = true; // 작업이 성공하는 시나리오를 가정
if (success) {
resolve("Data fetched successfully!");
} else {
reject("Failed to fetch data.");
}
}, 2000);
});
}
// 프로미스를 사용하여 비동기 작업 처리
fetchData()
.then((data) => {
console.log(data); // 출력: Data fetched successfully!
})
.catch((error) => {
console.log(error);
});
프로미스를 사용하지 않은 예제
// 콜백을 사용하는 비동기 작업 함수
function fetchData(callback) {
setTimeout(() => {
const success = true; // 작업이 성공하는 시나리오를 가정
if (success) {
callback(null, "Data fetched successfully!");
} else {
callback("Failed to fetch data.", null);
}
}, 2000);
}
// 콜백을 사용하여 비동기 작업 처리
fetchData((error, data) => {
if (error) {
console.log(error);
} else {
console.log(data); // 출력: Data fetched successfully!
}
});
먼저 프로미스를 사용하지 않고 콜백 함수로 비동기 함수를 작업한 예를 보면 fetchData 함수가 콜백 함수를 인수로 받아 비동기 작업이 완료되면 해당 콜백을 호출하게 됩니다.
콜백 함수의 첫 번째 인수는 오류 메시지(error), 두 번째 인수는 성공적인 결과(data)를 받게 됩니다.
방법은 간단하지만 여러개의 비동기 작업을 중첩해서 사용하면 코드가 복잡해지고, 가독성이 떨어지는 '콜백 지옥'에 빠지게 됩니다.
반면 프로미스를 사용한 경우 fetchData 함수는 Promise 객체를 반환하게 되는데 이 객체는 비동기 작업이 완료될 때까지 대기 상태를 유지하게 됩니다.
그 후 비동기 작업이 성공하면 resolve가 호출되어 프로미스가 '이행(fulfilled)'상태로 변경되고, 이 경우 then 메서드가 호출되면서 성공적인 결과(data)를 출력하게 됩니다.
만약 작업이 실패하면 reject가 호출되어 프로미스가 '거부(rejected)' 상태로 변경되고, catch 메서드가 호출되어 오류(error)를 처리하게 됩니다.
이 처럼 프로미스는 비동기 작업의 흐름을 명확하게 관리할 수 있을 뿐만 아니라 여러 개의 비동기 작업을 연속적으로 처리하는 것도 쉽습니다.
하지만!! 프로미스 역시 .then() 부분이 아래처럼 길어질 경우
fetchData()
.then((data) => {
console.log(data); // 출력: Data fetched successfully!
})
.then((data) => {
console.log(data); // 출력: Data fetched successfully!
})
.then((data) => {
console.log(data); // 출력: Data fetched successfully!
})
.then((data) => {
console.log(data); // 출력: Data fetched successfully!
})
.then((data) => {
console.log(data); // 출력: Data fetched successfully!
})
.then((data) => {
console.log(data); // 출력: Data fetched successfully!
})
.then((data) => {
console.log(data); // 출력: Data fetched successfully!
})
.then((data) => {
console.log(data); // 출력: Data fetched successfully!
})
.catch((error) => {
console.log(error);
});
이번엔 '콜백 지옥'이 아닌 then()지옥이 펼쳐지게 됩니다. 그래서 이를 해결하기 위해 추가된 기능이 있는데!!
7. asnyc/await
바로 asnyc/await입니다.
asnyc/await 는 JavaScript에서 비동기 작업을 더욱 직관적이고 간편하게 처리할 수 있도록 도와주는 ES8(ES2017)에서 도입된 문법입니다. (노드 7.6버전 부터 사용 가능한 기능입니다.)
async 키워드를 사용해 비동기 함수를 정의하고, await 키워드를 사용해 프로미스의 이행(또는 거부)을 기다리는 패턴입니다.
이것을 사용하면 비동기 코드가 마치 동기 코드처럼 읽히는 효과를 가지게 됩니다.
함수 앞에 async 키워드를 붙이면, 해당 함수는 항상 프로미스를 반환하는 함수로 변경되어 프로미스가 아닌 값을 반환하게 될 때 자동으로 이행된 프로미스로 감싸지게 됩니다.
wait 키워드는 async 함수 내에서만 사용할 수 있으며, 프로미스가 이행될 때까지 함수 실행을 일시 정지시키게 되는데 이행된 프로미스의 결과 값을 반환하고, 만약 프로미스가 거부되면 오류가 발생하게 됩니다.
async/await를 사용한 예
// 프로미스를 반환하는 함수
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const success = true; // 작업이 성공하는 시나리오를 가정
if (success) {
resolve("Data fetched successfully!");
} else {
reject("Failed to fetch data.");
}
}, 2000);
});
}
// async/await를 사용하여 비동기 작업 처리
async function getData() {
try {
const data = await fetchData();
console.log(data); // 출력: Data fetched successfully!
} catch (error) {
console.log(error);
}
}
getData();
async/await를 사용하지 않았을 때(프로미스만 사용)
// 프로미스를 반환하는 함수
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const success = true; // 작업이 성공하는 시나리오를 가정
if (success) {
resolve("Data fetched successfully!");
} else {
reject("Failed to fetch data.");
}
}, 2000);
});
}
// 프로미스를 사용하여 비동기 작업 처리
fetchData()
.then((data) => {
console.log(data); // 출력: Data fetched successfully!
})
.catch((error) => {
console.log(error);
});
추가로 async/await와 화살표 함수를 사용할 수 있고, 반복문을 통해 반복 호출을 할 수 있는데
이는 노드 10버전 부터 지원하는 ES20218 문법입니다.
// 프로미스를 반환하는 함수
const fetchData = (id) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const success = true; // 작업이 성공하는 시나리오를 가정
if (success) {
resolve(`Data fetched successfully for ID: ${id}`);
} else {
reject(`Failed to fetch data for ID: ${id}`);
}
}, 2000);
});
};
// 비동기 작업을 반복 호출
const getData = async (ids) => {
for (const id of ids) {
try {
const data = await fetchData(id);
console.log(data);
} catch (error) {
console.log(error);
}
}
};
// ID 배열
const ids = [1, 2, 3, 4];
// getData 호출
getData(ids);
만약 async/await에 대해 더 자세히 알고 싶다면 아래글을 추천드립니다.