2025. 5. 20. 09:00ㆍJavascript/Javascript
구조분해할당(Destructuring)
자바스크립트 개발을 하다 보면 객체를 함수끼리 주고 받는 상황이 아주 많다. 그로 인해 전달 받은 객체의 프로퍼티를 변수로 선언하려면 각 프로퍼티를 별도의 변수로 할당하기 위해 각 프로퍼티마다 독립된 할당문을 작성해야 했다. 하지만 구조분해할당이라고 불리는 문법이 추가되어 변수 선언이 훨씬 더 편리해졌고 코드가 간결해졌다.
변수 선언
먼저 변수의 프로퍼티를 쉽게 선언하는 예제이다. 객체의 구조분해할당은 변수로 선언하고자 하는 객체의 프로퍼티명을 { } 안에 나열하면 각 프로퍼티의 이름으로 변수가 생성되고 프로퍼티의 값이 자동으로 할당된다. 배열의 구조분해할당 또한 비슷한데 [ ] 안에 나열하는 변수의 이름에 맞는 인덱스의 요소가 변수의 값으로 할당된다.
ES5
function printUserInformation(data) {
var name = data.name;
var age = data.age;
var hobby = data.hobbies;
var firstHobby = hobbies[0];
console.log('이름: ' + name);
console.log('나이: ' + age);
console.log('가장 좋아하는 취미: ' + firstHobby);
}
ES6
function printUserInformation(data) {
const {name, age, gender, hobbies} = data;
const [firstHobby] = hobbies;
console.log(`이름: ${name}`);
console.log(`나이: ${age}`);
console.log(`가장 좋아하는 취미: ${firstHobby}`);
}
위 예시에서는 배열 구조분해할당으로 [ ] 접근자를 사용하지 않고도 변수를 선언했다. 그리고 객체 구조분해할당을 통해 반복되는 var * = data.*가 사라지고 한 줄짜리 간결한 코드로 바뀐 것을 볼 수 있다.
파라미터 내부 변수 선언
ES6에서는 파라미터로 받은 객체의 프로퍼티를 변수로 선언하여 사용할 수 있다. 이때는 별도의 변수 선언문 없이 파라미터의 위치에 구조분해할당 코드를 작성하면 된다. 선언할 변수의 이름은 기존 객체에 선언된 이름 말고 다른 이름으로도 선언 가능하다.
ES5
function printError(error) {
var errorCode = error.errorCode;
var msg = error.errorMessage;
console.log('에러코드: ' + errorCode);
console.log('메시지:' + msg);
}
ES6
function printError({
errorCode,
errorMessage: msg
}) {
console.log(`에러코드: ${errorCode}`);
console.log(`메시지: ${msg}`);
}
먼저 ES5 예제의 var * = data.* 같은 반복적인 코드 작성 부분이 객체 리터럴처럼 간결하게 바뀌었다. 그리고 printError() 함수의 파라미터를 구조분해할당하여 별도의 변수 선언 키워드를 사용하지 않았다. 또한 매개변수의 프로퍼티 이름 errorMessage를 :로 연결해서 변수명을 쉽게 바꿀 수 있다.
함수 파라미터의 기본 값 설정
기본 값 할당
ES6에서는 함수 파라미터의 기본 값 설정을 자바스크립트 문법에서 지원하게 되었다. 기본 값 설정이란 함수의 오동작을 막기 위해 특정 타입 혹은 값을 가져야 할 매개변수가 undefined로 전달된 경우, undfined 대신 사용할 수 있는 값을 할당하는 것이다. ES5에서는 기본 값을 설정하기 위해 if문으로 파라미터가 undefined인지 확인한 뒤 해당 파라미터의 값이 undefined라면 대체할 값을 해당 파라미터에 할당하는 방식으로 처리해 왔다. 하지만 ES6에서는 더욱 간결한 문법으로 해결할 수 있다.
ES5
function sayName(name) {
if (!name) {
name = 'World';
}
console.log('Hello, ' + name + '!');
}
sayName(); // "Hello, World!"
ES6
const sayName = (name = 'World') => {
console.log(`Hello, ${name}!`);
}
sayName(); // "Hello, World!"
구조분해할당 형태의 기본 값 할당
위에서 다룬 내용은 원시 타입에 대한 파라미터 기본 값 설정이다. 하지만 보다 복잡한 파라미터에 대한 기본 값 설정 또한 가능하다. ES5에서는 함수의 파라미터가 객체일 때 프로퍼티 값, 또는 파라미터 자체의 optional 처리를 위해 때로는 함수의 기능 구현보다 더 긴 코드를 작성해야 했다. 객체를 전달하는 함수가 많으면 매번 각 프로퍼티를 optional 처리해 주는 것이 상당히 귀찮은 작업의 연속이었지만, 구조분해할당과 유사한 형태의 문법으로 함수 파라미터의 기본 값을 간결하게 설정할 수 있다.
ES5
function drawES5Chart(options) {
options = options || {};
var size = options.size || 'big';
var cords = options.cords || {x: 0, y: 0};
var radius = options.radius || 25;
console.log(size, cords, radius);
// now finally do some chart drawing
}
drawES5Chart({
cords: {x: 18, y: 30},
radius: 30
});
ES6
function drawES6Chart({size = 'big', cords = {x: 0, y: 0}, radius = 25} = {}) {
console.log(size, cords, radius);
// do some chart drawing
}
drawES6Chart({
cords: {x: 18, y: 30},
radius: 30
});
함수 파라미터의 기본 값 설정 시 주의해햐 할 점이 있다. 만약 파라미터 안에 있는 객체의 프로퍼티 중 일부만 기본 값으로 처리를 하고 싶은 경우가 있다고 가정해 보자. 다시 말해 2-depth의 기본 값 처리.
// Bad
function drawES6Chart({size = 'big', cords = {src: {x: 0, y: 0}, dest: {x: 0, y: 0}}, radius = 25} = {}) {
console.log(size, cords.src.x, cords.src.y, cords.dest.x, cords.dest.y, radius);
}
drawES6Chart({
cords: {src: {x: 18, y: 30}},
radius: 30
}); // 에러: undefined의 x, y를 참조하려고 해서 에러 발생.
위 코드는 잘 동작할 것처럼 보이지만 에러가 발생한다. 왜일까? 함수의 파라미터로 넘어온 cords가 {src: {x: 18, y: 30}}로 채워져 있기 때문에 함수 실행 시 파라미터 기본 값 설정 부분이 실행되지 않는 것이다. 그러므로 cords.dest는 undfined가 되므로 dest의 x, y 프로퍼티를 읽게 되면 undefined의 프로퍼티에 접근하게 되어 참조 에러가 발생한다.
앞서 보았듯 파라미터의 기본 값 설정은 1-depth, 즉 파라미터 자체의 프로퍼티까지만 지원한다. 위 예제 코드를 정상 동작하게 하려면 ES5 버전의 코드처럼 다시 2-depth부터 각각 optional 처리를 해줘야 한다.
// Good
function drawES6Chart({size = 'big', cords = {src: {x: 0, y: 0}, dest: {x: 0, y: 0}}, radius = 25} = {}) {
if (cords.src === undefined) {
cords.src = {x: 0, y: 0};
}
if (cords.dest === undefined) {
cords.dest = {x: 0, y: 0};
}
console.log(size, cords.src.x, cords.src.y, cords.dest.x, cords.dest.y, radius);
}
drawES6Chart({
cords: {src: {x: 18, y: 30}},
radius: 30
}); // 정상 동작.
2-depth 이상의 파라미터 기본 값 설정 시에만 주의하여 사용한다면 기존 ES5 코드보다는 더 간결하고 읽기 쉬운 코드로 유지할 수 있다.
Rest 파라미터, Spread 표현식
ES6는 객체 리터럴이나 배열 리터럴의 사용성이 대폭 좋아졌다. Rest 파라미터나 Spread 표현식도 그 중 하나이다.
Spread 표현식
배열이나 객체 리터럴 내부에 ...ids와 같이 작성하면 해당 위치에 ids의 배열 요소나 프로퍼티를 풀어낸다. Spread 표현식은 함수의 호출이나 배열 및 객체 리터럴 내부에서 사용할 수 있다. 따라서 배열 복사나 불변(immutable) 객체 생성도 손쉽게 할 수 있다. ...연산자와 함께 풀어낼 객체를, 그리고 그 뒤에 추가/변경될 내용을 작성하면 된다.
배열을 함수 파라미터로 변경하기
배열을 함수의 파라미터들로 변경할 때 Spread 표현식으로 편리학 작성할 수 있다.
ES5
var friends = ['Jack', 'Jill', 'Tom'];
textToFriends(friends[0], friends[1], friends[2]);
ES6
const friends = ['Jack', 'Jill', 'Tom'];
textToFriends(...friends);
배열 및 객체 확장
새로운 배열에 다른 배열의 요소를 한 번에 추가하거나 새로운 객체에 다른 객체의 프로퍼티들을 추가할 때에도 코드가 훨씬 깔끔하게 유지된다. 새로운 객체를 만드는 경우 Spread 표현식의 계산 결과로 인해 중복되는 키가 생긴다면 가장 나중에 작성된 표현식이 할당된다.
ES5
var friends = ['Jack', 'Jill', 'Tom'];
var anotherFriedns = [friends[0], friends[1], friends[2], 'Kim'];
var drinks = {
coffee: 'coffee',
juice: 'orange juice'
};
var newDrinks = {
coffee: drinks.coffee,
juice: 'tomato juice',
water: 'water'
};
ES6
const friends = ['Jack', 'Jill', 'Tom'];
const anotherFriedns = [...friends, 'Kim'];
const drinks = {
coffee: 'coffee',
juice: 'orange juice'
};
const newDrinks = {
...drinks,
juice: 'tomato juice',
water: 'water'
};
Rest 파라미터
이전까지는 파라미터의 개수가 가변적인 함수에서 파라미터들을 사용하려면 arguments 객체를 배열처럼 접근해서 사용해야 했다. 하지만 someFunction(target, ...params) 형태로 Rest 파라미터 연산자를 작성하면 target 뒤에 오는 파라미터들을 모두 params 배열로 쉽게 바꿀 수 있다. 모든 인수를 바꿀 수도 있고 다음과 같이 앞서 선언한 변수를 제외한 파라미터들만 배열로 변환할 수도 있다.
ES5
function textToFriends() {
var message = arguments[0];
var friends = [].slice.call(arguments, 1); // argunemts 2번째 요소부터 친구들 배열로 만들기.
console.log(message, friends);
}
ES6
function textToFriends(message, ...friends) {
console.log(message, friends);
}
ES5 코드처럼 arguments 객체를 배열처럼 사용하지 않더라도 매개변수들을 변수와 배열로 분리하여 사용할 수 있다.
제너레이터(Generator)
ES6의 제너레이터는 Generator 생성자나 function* 키워드로 선언한다. 제너레이터는 코드의 진행 흐름에서 잠시 빠져나갔다가 다시 돌아올 수 있는 함수이다.
문법
function* gen() {
yield 1;
yield 2;
yield 3;
yield 4;
}
const g = gen();
제너레이터를 실행하면 yield를 만날 때까지 코드를 수행하고 대기하며 제어가 다시 제너레이터를 실행한 다음 라인으로 넘어간다. 멈춰 있는 제너레이터를 재개하려면 제너레이터 객체의 g.next() 메서드를 실행하면 된다. 제너레이터의 g.next() 메서드를 수행하면 멈춰 있던 위치의 yield에서부터 다음 yield문을 만날 때까지 코드를 수행한다. 그리고 다시 제어가 제너레이터를 빠져나와 g.next() 메서드를 실행한 다음 라인으로 넘어간다. g.next()의 리턴 값은 객체이며 제너레이터가 모두 수행되었는지를 알려주는 boolean 값 done과 yield문의 수행 결과값인 value 프로퍼티로 구성되어 있다.
function* gen() {
yield 1;
yield 2;
yield 3;
yield 4;
}
const g = gen();
console.log(g.next().value); // 1
console.log(g.next().value); // 2
console.log(g.next().value); // 3
console.log(g.next().value); // 4
console.log(g.next().value); // undefined
위 예시에서 마지막 g.next().vlaue가 undefined인 이유는 모든 yield 구문이 수행되어 제너레이터가 종료되었기 때문이다.
프로미스(Promise)
프로미스는 비동기 처리가 추상화된 객체이다. 사용자가 작성한 비동기 처리가 완료되거나 실패되었는지 알려주고 비동기 처리 결과값을 반환해 준다. 이를 통해 성공 시 실행할 함수, 실패 시 실행할 함수를 등록해서 편리하게 비동기 처리 코드 작성이 가능하다. 프로미스를 이용하면 비동기 처리를 위한 콜백 함수들로 여러 겹 감싸진 콜백 지옥 코드를 간결하게 작성할 수 있다. 문법부터 천천히 살펴 보기로 하자.
문법
const p = new Promise((resolve, reject) => {
// 비동기가 처리 필요한 코드
});
p.then(onFulfilled, onRejected).catch(errorCallback);
프로미스 생성자에 전달되는 함수 파라미터는 실행자(excutor)라고 하며, 실행자는 프로미스 생성자가 생성한 객체를 리턴하기 전에 실행된다. 실행자의 인자인 resolve와 reject는 프로미스 구현에 의해 실행자에 파라미터로 전달되는 함수이며 프로미스를 해결하거나 거부하는 함수이다. 이 두 개의 인자를 이용해서 실행자 내부의 비동기 처리의 결과를 판단하고 resolve나 reject 함수에 후속 처리를 위해 전달할 값을 인자로 넘겨주면서 실행하여 프로미스가 완료되도록 하면 된다. 만약 실행자 내부에서 resolve가 실행되면 then의 첫 번째 인자인 onFulfilled가 받게 되고 반대로 reject가 실행되면 두 번째 인자 onRejected가 받게 된다.
const checkNumIsExceedTen = new Promise((resolve, reject) => {
setTimeout(() => {
const num = getRandomNumberFromServer();
if(Number.isNaN(num)) {
throw new Error('Value that from server must be a "number" type.');
}
if (num > 10) {
resolve(num);
} else {
reject(num);
}
});
});
checkNumIsExceedTen
.then((num) => {
console.log(`'num' is ${num}. It is exceed 10.`);
}, (num) => {
console.log(`'num' is ${num}. It is not exceed 10.`);
})
.catch((error) => {
console.log(error.message);
});
위 예시에서는 서버에서 가져온 num 변수의 값이 10을 초과하는지 확인하는 프로미스 객체를 생성했다. 그리고 프로미스가 종료된 후 실행할 콜백들을 then을 통해 등록했고 에러가 발생했을 때 에러를 출력할 콜백도 catch를 이용해 등록했다.
콜백 지옥 개선
기존 ES5에서는 비동기 처리를 하기 위해 보통 콜백 지옥 또는 콜백 피라미드라고 하는 형태의 코드를 작성했다. 어떤 비동기 처리의 결과를 전달 받는 함수를 콜백 함수의 형태로 계속 생성하고 최종 결과를 가장 안쪽의 콜백 함수에서 전달 받아 실행이 종료된다.
ES5
doSomething(function(result) {
doSomethingElse(result, function(newResult) {
doThirdThing(newResult, function(finalResult) {
console.log('Got the final result: ' + finalResult);
}, failureCallback);
}, failureCallback);
}, failureCallback);
ES6
doSomethingPromise
.then((result) => doSomethingElse(result))
.then((newResult) => doThirdThing(newResult))
.then((finalResult) => console.log('Got the final result: ' + finalResult))
.catch(failureCallback);
겹겹이 쌓여 있던 콜백 함수 코드가 훨신 간단해졌다. 두 코드의 차이를 보자. 콜백 지옥 코드에서는 함수마다 에러 처리 콜백을 전달했다면, 프로미스 코드에서는 한 번의 catch()로 해결된다. 또한 겹겹이 쌓여가는 콜백 함수와 비교하면 프로미스는 비동기 처리들을 순서대로 연결해서 읽기 쉽게 작성할 수 있다.
여러 개의 프로미스 처리하기
프로미스는 단일 비동기 요청을 다루기 위한 객체이다. 여러 개의 비동기 요청을 처리하기 위해서는 프로미스 객체를 여러 개 사용해야 한다. 이때 Promise.all, Promise.race를 사용하면 객체들이 완료되는 상태에 따라 처리할 수 있게 된다. 함수의 파라미터로는 배열 같이 순회 가능한(iterable) 객체를 받는다. Promise.all은 모든 프로미스가 resolve될 때까지 기다리고 Promise.race는 가장 먼저 resolve되는 프로미스의 이행 값을 사용한다. 각 프로미스는 순차 처리되는 것이 아니라 병렬적으로 수행된다.
var p1 = Promise.resolve(3);
var p2 = 1337;
var p3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("foo");
}, 100);
});
Promise.all([p1, p2, p3]).then(values => {
console.log(values); // [3, 1337, "foo"]
});
Promise.race([p1, p2, p3]).then(value => {
console.log(value); // 3
});
순회 가능한 객체의 인자가 모두 resolve되면 resolve된 프로미스를 반환하고 하나라도 reject되면 첫 번째로 reject된 이유를 사용하여 reject 프로미스를 반환한다.
async/await
async 함수를 이용하면 비동기 처리를 더욱 간결하게 작성할 수 있다. async 함수는 여러 개의 프로미스를 사용하는 코드를 동기 함수 실행과 비슷한 모습으로 사용할 수 있게 해 준다. async 키워드가 앞에 붙은 함수 선언문의 함수 본문에는 await 식이 포함될 수 있다. 이 await 식은 async 함수 실행을 일시 중지하고 표현식에 전달된 프로미스의 해결을 기다린 다음 async 함수의 실행을 재개하고 함수의 실행이 모두 완료된 뒤에 값을 반환한다. 물론 await은 async 함수 내에서만 유효하다. 외부에서 사용한다면 문법 에러가 발생한다.
async함수의 반환 값은 프로미스이며 예를 들어 returnValue를 반환하면 암묵적으로 Promise.resolve(returnValue) 형태로 감싸져서 반환된다. 프로미스에 catch로 처리하던 에러는 일반 함수에서의 try/catch문으로 작성하면 된다. 에러가 발생하면 프로미스의 reject에 전달되는 값이 에러 객체로 넘어온다.
프로미스
function fetchMemberNames(groupId) {
return getMemberGroup(groupId)
.then(memberGroup => getMembers(memberGroup))
.then(members => members.map(({name}) => name))
.catch(err => {
showNotify(err.message);
});
}
fetchMemberNames('gid-11')
.then(names => {
if (names) {
addMembers(names);
}
});
async 함수
async function fetchMemberNames(groupId) {
try {
const memberGroup = await getMemberGroup(groupId);
const members = await getMembers(memberGroup);
return members.map(({name}) => name);
} catch (err) {
showNotify(err.message);
}
}
fetchMemberNames('gid-11')
.then(members => {
if (members) {
addMembers(members);
}
});
모듈(ES Module)
자바스크립트로도 모듈 개발이 가능하다. ES5에서는 Webpack, Rollup, Parcel 같은 번들러나 Babel 같은 트랜스파일러를 사용해서 브라우저에서 실행할 수 있도록 바꿔줘야 했다. 그러나 ES6에서는 모듈을 이용해서 개발할 수 있는 간결한 문법을 지원한다. 모듈 명세를 구현한 모던 브라우저들부터는 import, export문을 사용해서 모듈을 가져올 수 있다.
자바스크립트의 모듈은 .js 확장자로 만들어진 파일을 뜻한다. 파일 내부에서 별도의 module 등의 키워드로 선언할 필요가 없으며, 자바스크립트 모듈의 코드는 기본적으로 strict 모드로 동작한다. 모듈 안에서는 import, export 키워드를 통해 다른 모듈과 객체를 주고받을 수 있다. 하나의 모듈 안에서 선언된 변수나 함수 등은 그 모듈 내부 스코프를 가진다.
그렇다면 우리가 작성한 모듈의 변수를 다른 모듈에서 사용하려면 어떻게 해야 할까? 바로 export를 통해 모듈 외부에서 접근할 수 있도록 만들어주면 된다. export문을 통해 함수, 클래스, 변수 등을 모듈 외부로 내보낼 수 있다. 그렇다면 이제 export문을 사용하는 방법부터 살펴보자. 모듈 외부로 내보내는 방법에는 Named export, Default export 두 가지가 있다.
Named export
// students.js
export const student = 'Park';
export const student2 = 'Ted';
const student3 = 'Abby';
export {student3};
Named export는 한 파일에서 여러 번 할 수 있다. Named export를 통해 내보낸 것들은 추후 다른 모듈에서 내보낼 때와 같은 이름으로 import해야 한다.
Default export
// studentJack.js
export default 'Jack'
반면 Default export는 한 스크립트 파일당 한 개만 사용할 수 있다. 그리고 export default 뒤에는 표현식만 허용되므로 var, let, const 등의 키워드는 사용하지 못한다.
이렇게 내보낸 객체들은 모듈들에서 접근할 수 있다. 그렇다면 지금부터는 모듈에서 export한 객체들을 가져오는 import문을 살펴보자.
Named export된 객체 사용하기
import {student, student2, student3} from 'students.js';
console.log(student); // "Park";
console.log(student2); // "Ted";
console.log(student3); // "Abby";
위처럼 Named export된 객체를 가져올 때에는 각 객체의 이름들을 { }로 감싸면 된다. 만약 가져올 객체의 이름을 바꿔서 가져오고 싶을 때는 어떻게 해야 할까? 별도의 변수를 선언하지 않더라도 바꾸고 싶은 객체 이름 뒤에 as [[바꿀 변수명]] 형태로 작성해서 쉽게 바꿀 수 있다.
import {student as park, student2 as ted, student3 as abby} from 'students.js';
const student = 'Kim';
console.log(student); // "Kim"
console.log(park); // "Park"
console.log(ted); // "Ted"
console.log(abby); // "Abby"
이 방법은 이미 작성한 코드의 지역 변수명과 같은 이름의 객체를 가져올 때 유용하게 사용할 수 있다.
그럼 이렇게 Named export된 객체가 많을 때 모두 가져오려면 반드시 위 예제처럼 각 객체를 하나씩 열거해야 할까? 아니다. * 을 이용해서 한꺼번에 가져오는 방법이 있다.
import * as students from 'students.js';
console.log(students.student); // "Park"
console.log(students.student2); // "Ted"
console.log(students.student3); // "Abby"
이번에도 * 문법으로 students.js 파일 내부의 모든 Named export 객체를 나타내주고, 바로 뒤에 as [[변수명]] 형태로 해당 객체들을 가지고 있을 변수명을 정한다.
Default export된 객체 사용하기
import jack from 'studentJack';
console.log(jack); // "Jack"
사용법은 Named export와 비슷하지만 { }로 감싸지 않고 변수명을 import문 뒤에 작성한다. 변수 이름은 바꿔서 가져올 수 있는데 Default export된 객체는 파일마다 유일하므로 as 키워드를 사용하지 않더라도 이름을 바꿔서 불러올 수 있다. import 키워드 뒤에 사용한 이름이 객체의 변수명이 된다.
두 방법 모두 사용하기
앞서 소개한 객체를 가져오는 방법 Named export와 Default export는 export와 마찬가지로 가져올 대도 한 번에 두 가지를 모두 사용할 수 있다. 한 students.js 파일에서는 객체를 내보내고 school.js 파일에서는 그 객체들을 가져와 보자.
// students.js
const jack = 'Jack';
export default jack
const student = 'Park';
const student2 = 'Ted';
const student3 = 'Abby';
export {student, student2, student3};
// school.js
import jack, {student, student2, student3} from 'students';
console.log(jack); // "Jack"
console.log(student); // "Park"
console.log(student2);// "Ted"
console.log(student3);// "Abby"
'Javascript > Javascript' 카테고리의 다른 글
[Javascript] 함수의 기본 파라미터 (1) | 2025.05.21 |
---|---|
[Javascript] 함수 표현식 vs 함수 선언문 (1) | 2025.05.20 |
[Javascript] ECMAScript 2015(ES6)+ (1) (3) | 2025.05.19 |
[Javascript] && 연산자로 조건문 단축시키기 (1) | 2025.05.16 |
[Javascript] Scope (1) | 2025.05.15 |