const는 문맥 내에서 재할당할 수 없는 변수 선언이다. 재할당 할 수 없는 값을 const로 한다면 후에 코드를 확인할 때 무엇이 바뀌지 않는 값인지 구분할 수 있다.
단, const에 배열을 할당하는 경우 배열의 항목은 바뀔수 있다. 이런 조작을 피하기 위해 filter 등 배열의 값을 새로운 배열로 반환하는 메서드를 이용하는 것이 좋다.
재할당을 해야 하는 변수의 경우에는 let을 사용한다. let은 블록 유효 범위를 따르기 때문이다. 즉, 변수를 선언한 중괄호를 벗어나면 변수가 존재하지 않는다.
for문에서 (var i; i<5; i++)와 같이 i값을 var로 선언하면 루프를 돌 때 마지막 i의 값만 저장된다.
하지만 (let i; i<5; i++)와 같이 i값을 let으로 선언하면 각 i의 값이 저장된다. 반복할 때마다 다른 i 값이 전달되는 것이다. 쉽게 말해 let을 이용하면 for 문이 반복될 때마다 값을 잠근다.
일반적인 문자열, 함수에서 반환된 문자열, 변수에 할당된 문자열 등을 연결할 때 ''와 +로 연결하면 가독성이 떨어지고 복잡해진다. 이 때 템플릿 리터럴 ${}과 백틱(``)을 사용하면 가독성이 좋고 간단해진다.
중괄호 안에서는 메서드를 호출할 수도 있고, 계산식을 넣을 수도 있다. 하지만 중괄호 내부에서 많은 것을 하면 어수선해지기 때문에 가급적 템플릿 리터럴 외부에서 처리하고 결괏값을 변수에 할당해 사용하는것이 좋다.
어떤 형태로든 데이터를 추가, 제거, 정렬, 필터링, 교체 등 조작해야 한다면 배열이 가장 적합한 컬렉션이다.
배열은 순서를 갖기 때문에 순서를 기준으로 값을 추가하거나 제거할 수 있고, 모든 위치에 값이 있는지 확인할 수 있다. 또한 map(), filter(), reduce() 등의 배열 메서드를 이용하면 코드 한 줄로 정보를 쉽게 변경하거나 갱신할 수 있다. 펼침 연산자로 새로운 배열을 생성할 수 있고, 이를 이용해 맵도 생성할 수 있다. 객체를 키-값 쌍을 모이둔 배열로 바꿀 수도 있다는 것도 알아두자.
배열과 프록시를 이용한 이터러블을 이해하면 ES6의 새로운 기능뿐만 아니라 ES6 이후에 소개될 새로운 기능도 확실하게 파악할 수 있을 것이다.
includes() 메서드가 있기 전에는 indexOf() 메서드를 이용해 존재 여부를 확인했는데, 이는 다소 번거롭기도 하고 또는 색인이 0일 경우 존재하는 값이어도 결과가 false로 평가가 될 수 있는 문제점이 있었다. 따라서 아래와 같이 숫자와 비교하는 과정을 거쳐야 했다.
function checkItem(section) {
return section.indexOf('item') > -1;
}
새롭게 추가된 includes() 메서드를 사용하면 값이 배열에 존재하는지 여부를 확인해서 boolean 값으로 반환한다. 아래와 같이 사용할 수 있다.
function checkItem(section) {
return section.includes('item');
}
펼침 연산자는 마침표 세 개(...)로 표시하며 배열에 포함된 항목을 목록으로 바꿔준다. 펼침 연산자는 배열 뿐만 아니라 맵 컬렉션에서도 사용한다. 또한 제너레이터를 이용하는 데이터 구조나 클래스 속성에도 사용할 수 있다.
펼침연산자는 단독으로 사용할 수 없으며 어디든 펼쳐 넣어야 한다.
배열 항목 제거
for문과 push() 사용 --> 코드가 복잡하고 길다
indexOf(), splice() 사용 --> splice가 원본 배열을 조작한다.
indexOf(), slice(), concat() 사용 --> 무엇이 반환되는지 정확하지 않다.
이 때 펼침 연산자와 함께 사용하기에 적합하다. 조작이 없고, 읽기 쉽고 간결하며 재사용할 수 있으며 예측 가능하다.
함수의 인수 목록을 생성할 때도 펼침 연산자를 활용할 수 있다.
const book = ['book title', 'book author', '3,000'];
function format(title, author, price) {
return `${title} by ${author} $${price}`;
}
format(...book);
push() 메서드는 원본 배열을 조작하므로 펼침 연산자를 이용해 새로운 배열을 만든다.
펼침 연산자는 배열을 복사한다. slice() 메서드 또한 배열을 복사할 수 있다.
const cart = ['item1', 'item2', ...];
cart.push(reward); // 원본에 추가
[...cart, reward]; // 복사된 배열에 추가
sort() 메서드로 배열을 정렬할 때도 원본 배열이 조작된다.
이를 방지하기 위해 펼침 연산자를 사용한다.
const arr = ['item1', 'item2', ...];
arr.sort(); // 원본 조작
[...arr].sort(); // 복사본 조작
값의 의미를 명시해야 하며, 순서가 중요하지 않을 때는 키-값 컬렉션이 더 적절하다. 대부분은 객체를 사용하는데, 객체는 훌륭하지만 복잡하다. 객체는 키-값 컬렉션으로 사용할 수도 있지만, 생성자, 메서드, 속성을 가진 클래스에 좀 더 가깝게 사용할 수도 있다. 그렇다면 어떠한 경우에 객체를 선택하는 것이 최선인지 알아야 한다.
원칙적으로 객체는 변화가 없고 구조화된 키-값 데이터를 다루는 경우에 유용하다. 반면에 자주 갱신되거나 실행되기 전에 알 수 없는 동적인 정보를 다루기에는 적합하지 않다. 핵심은 객체가 정적인 정보에 적합하다는 것이다.
// 배열로 담았을 때 값의 의미를 알 수 없다.
const colorsArr = ['#d10202', '#19d836', '#0e33d8'];
// 객체로 담아서 값의 의미를 명시해준다.
const colorsObj = {
red: '#d10202',
green: '#19d836',
blue: '#0e33d8'
}
정적인 객체도 프로그래밍적으로 정의할 수 있다. 예를 들어 함수 내에서 객체를 생성하고 다른 함수에 넘겨줄 수 있다. 정보를 수집하고 전달해 다른 함수에서 사용하는 것이다. 그 비결은 데이터를 매번 같은 방식으로 설정하고 사용하는 것이다. 기존의 객체를 조작하는 것이 아니라 각각의 함수에서 새로운 객체를 생성한다. 객체는 전달받는 함수에서 구조를 미리 알고 있고 변수를 이용해 키를 설정하지 않는다.
또한 함수의 각 항목을 매개변수로 받는 대신, 객체를 전달해 필요한 값을 꺼내 쓸 수 있도록 했다. 이런 경우 객체는 다른 컬렉션에 비해 빠르고 명로할 뿐만 아니라, 객체 해체 할당도 가능해서 객체로 데이터를 다루는 것이 좋다.
객체도 배열과 마찬가지로 조작에 관련된 문제가 생길 수 있다. 흔한 예로, 키-값 쌍이 여러 개인 객체가 있는데, 객체가 완전하지 않다. 기존 데이터가 있는 상태에서 새로운 필드를 추가해야 하거나, 외부 API에서 데이터를 가져와 현재 데이터 모델에 연결해야 하는 경우 자주 발생한다. 기본값을 설정하면서, 원래 데이터는 유지하는 새로운 객체를 생성하려면 Object.assign() 메서드를 사용한다.
const defaults = {
author: '',
title: '',
year: 2017,
rating: null,
};
const book = {
author: 'Joe Morgan',
title: 'Symplifying JavaScript',
}
Object.assign(defaults, book);
하지만 위 코드는 원본 객체를 조작시킨다. 이런 문제를 피하려면 첫 번째 객체에 빈 객체를 사용해야 한다.
Object.assign({}, defaults, book);
한 가지 문제는 객체 안에 중첩된 객체가 복사되지 않는다는 것이다.
중첩된 객체가 있는 객체를 복사하는 것을 깊은 복사(deep copy) 또는 깊은 병합(deep merge)이라고 한다. 중첩된 객체를 복사하려면 다음과 같이 한다.
const defaults = {
name: {
first: '',
last: '',
}
years: 0,
}
const result = Object.assign(
{},
defaults,
{name: Object.assign({}, defaults.name)}
)
export { defaults };
또는 로대시(Lodash) 라이브러리의 경우 cloneDeep() 이라는 메서드를 이용할 수 있다.
객체 펼침 연산자는 배열 펼침 연산자와 비슷하다. 객체 펼침 연산자는 키-값 쌍을 목록에 있는 것처럼 반환한다. 새로운 정보는 연산자의 앞이나 뒤에 추가할 수 있다. 또한 배열 펼침 연산자와 마찬가지로 독립적으로 사용할 수는 없고 객체에 펼쳐지게 해야 한다.
배열 펼침 연산자와 다른 점은, 동일한 키에 서로 다른 값을 추가하면 가장 마지막에 선언된 값을 사용한다는 것이다. 이는 작성할 코드가 크게 줄어든 Object.assign()과 같다.
const book = {
title: 'title1',
author: 'name',
};
const update = {...book, title : 'title2'};
Object.assign()을 이용했던 코드를 다음과 같이 쓸 수 있다.
const defaults = {
author: '',
title: '',
year: 2017,
rating: null,
};
const book = {
author: 'Joe Morgan',
title: 'Symplifying JavaScript',
}
const bookWithDefaults = {...defaults, ...book};
객체 펼침 연산자도 깊은 복사가 되지 않는 문제가 있다. 다행히 좀 더 보기 좋게 문제를 해결할 수 있다.
const defaults = {
name: {
first: '',
last: '',
}
years: 0,
}
const result = Object.assign(
...default,
name: {
...default.name,
}
)
export { defaults };
맵(Map)은 객체를 대체할 수 있다. 맵을 컬렉션으로 선택하는 것이 더 나은 상황은 다음과 같다.
키-값 쌍이 자주 추가되거나 삭제되는 상황으로 필터링 조건 목록이 있다.
필터링 조건 기능을 맵으로 나타내면 다음과 같다.
객체와 달라 맵에서는 항상 명시적으로 새로 인스턴스를 생성해야 한다.
let filters = new Map();
인스턴스를 생성한 후에는 set() 메서드를 이용해서 데이터를 추가한다.
filters.set('견종','래브라도레트리버');
데이터를 가져오려면 get() 메서드를 사용한다. 인수로는 키만 전달한다.
filters.get('견종');
// '래브라도레트리버'
메서드를 차레로 연결해서 여러 값을 쉽게 추가할 수 있다. 이런 방법을 체이닝이라고 한다.
let filters = new Map()
.set('견종', '래브라도레트리버')
.set('크기', '대형견')
.set('색상', '갈색');
filters.get('크기');
// '대형견'
배열을 이용해서 정보를 추가할 수도 있다. 새로운 맵을 생성하고 set() 메서드를 길게 연결하는 대신에 키-값 쌍 배열을 전달하면 첫 번째 항목은 키, 두 번째 항목은 값으로 추가된다.
let filters = new Map(
[
['견종', '래브라도레트리버'],
['크기', '대형견'],
['색상', '갈색']
]
)
filters.get('색상');
// 갈색
맵에서 값을 제거할 때는 delete() 메서드를 사용한다.
filters.delete('색상');
filters.get('색상');
// undefined
마찬가지로 모든 키-값 쌍을 제거할 때는 clear() 메서드를 사용한다.
filters.clear();
filters.get('색상');
// undefined
이렇게 맵의 메서드를 이용하면 객체 대신 맵을 사용하는 함수를 만들 수 있다.
const petFilters = new Map();
function addFilters(filters, key, value) {
filters.set(key, value);
}
function deleteFilters(filters, key) {
filters.delete(key);
}
function clearFilters(filters) {
filters.clear()
}
맵을 사용한 위 함수에는 다음과 같은 특징이 있다.
덧붙여서, 객체의 경우 키에 정수를 사용할 수 없다.
const error = {
100: '이름이 잘못 되었습니다',
110: '이름에는 문자만 입력할 수 있습니다.',
200: '색상이 잘못되었습니다',
}
위와 같은 객체에서 errors.100을 호출하면 에러가 나게 된다. 키가 문자열로 변경되었기 때문이다. 이 때 맵을 사용하면 이러한 문제가 없다.
let errors = new Map([
[100, '이름이 잘못되었습니다.'],
[110, '이름에는 문자만 입력할 수 있습니다.'],
[200, '색상이 잘못되었습니다.']
])
error.get(100);
// 이름이 잘못되었습니다.
맵도 객체와 마찬가지로 키만 모아서 확인할 수 있다.
errors.key();
// MapIterator {100, 110, 200}
Object.keys()를 적용한 것과는 다르게 배열이 반환되지 않는다. 이 반환 값은 맵이터레이터라고 부른다.
내용은 같지만 자료형이 서로 다른 값을 비교할 때 == 을 이용하여 동등한지 확인한다.
값과 자료형까지 동일한지 비교하려면 === 을 이용한다.
거짓 값의 목록은 다음과 같다.
배열과 객체의 경우 빈 배열과 빈 객체라도 항상 참 값이다. 따라서 객체 또는 배열이 비어있는지 확인하려면 [].length 또는 Object.keys({}).length처럼 0 또는 참 값인 숫자를 반환하는 다른 방법을 사용해야 한다.
참과 거짓 값에 관심을 가져야 하는 이유는 긴 표현식을 축약할 수 있기 때문이다. 참, 거짓 값으로 값의 존재 여부를 확인할 수 있다. 가장 흔한 문제는 색인을 사용해서 배열에서 존재 여부를 검사하는 경우이다. indexOf()의 경우 inclues()를 사용해서 해결할 수 있다.
정의되지 않은 값을 가져오면 undefined가 되는데 이 경우 코드의 다른 곳에서 객체 또는 맵을 변경하는 부분이 있다면 문제가 생길 수 있다.
if(!employee.training) {
return '권한이 없습니다.';
} else {
return '반갑습니다.';
}
만약 employee.traning 값이 삭제되어 정의되지 않은 상태라면, undefined가 되어 '권한이 없습니다'라고 출력될 것이다. 만약 employee.training 값이 있고, true가 아닐 때만 '권한이 없습니다'라고 출력하려면 아래와 같이 써야 한다. 엄격한 일치를 이용하는 것이다.
if(employee.training !== true) {
return '권한이 없습니다.';
} else {
return '반갑습니다.';
}
간단한 조건문의 경우 삼항 연산자를 사용하면 코드를 한 줄로 줄일 수 있고, 예측 가능하며, 변수의 재할당을 줄일 수 있다.
// if문
if (active) {
var display = 'bold'
} else {
var display = 'normal'
}
// 삼항 연산자
var display = active ? 'bold' : 'normal';
새로운 변수 선언 방식은 블록 유효 범위 변수이기 때문에 if문 블록 밖에서는 결과를 확인할 수 없다. 이렇게 되면 블록 유효 범위 밖에서도 접근 가능한 var 변수를 사용하거나, let으로 변수를 선언하고 if/else 문 내에서 재할당 해야한다. 삼항 연산자는 이런 문제를 해결해준다.
삼항연산자에 같은 값이 반복되고 있다면 단락 평가를 이용해 더 단축할 수도 있다.
function getInconPath(icon) {
const path = icon.path ? icon.path : 'uploads/default.png';
return `https://aswwets.foo.com/${path}`;
}
위 코드에서는 icon.path가 반복되고 있다. 이 때 단락평가를 이용하면 다음과 같다.
function getInconPath(icon) {
const path = icon.path || 'uploads/default.png';
return `https://aswwets.foo.com/${path}`;
}
이처럼 단락 평가의 가장 좋은 점은 표현식의 끝에 기본값을 추가할 수 있다는 것이다. 따라서 변수가 거짓 값이 될 가능성을 염려하지 않아도 된다.
단락 평가를 이용하는 인기 있는 방법은 오류를 방지하는 것으로, 특정 컬렉션의 메서드 또는 동작을 사용할 때 단락 평가를 사용하는 것이다.
아래는 두가지 조건을 만족할 때만 동작이 실행되도록 한다.
function getImage(userConfig) {
if (userConfig.images && userConfig.images.length > 0) {
return userConfig.images[0];
}
return 'default.png';
}
위 코드를 삼항 연산자와 함께 사용하면 더욱 단축시킬 수 있다.
function getImage(userConfig) {
const images = userConfig.imgaes;
return imgaes && images.length ? imgaes[0] : 'default.png'
}
삼항 연산자와 단락 평가를 함께 사용할 때는 주의가 필요한데, 조건이 늘어날 경우 코드가 복잡해질 수 있기 때문이다. 코드가 길고 복잡해질 경우 독립적인 함수를 만드는 것이 낫다.
화살표 함수는 function 키워드 대신 => 기호를 사용한다. 매개변수가 하나 이하이고 코드가 한 줄이라면 인수를 감싸는 괄호, return 키워드, 중괄호도 사용하지 않아도 된다.
// 일반적인 함수
function formatUser(name) {
return `${capitalize(name)}님이 로그인했습니다.`;
}
// 화살표 함수
const formatUser = name => `${capitalize(name)}님이 로그인했습니다.`;
함수가 콜백 함수를 인자로 받을 때 익명함수를 생성하여 전달하기도 하는데 이 때 화살표 함수를 사용하면 간편하다.
// 일반적인 함수
applyCustomGreeting('mark', function (name) {
return `안녕 ${name}`;
})
// 화살표 함수
applyCustomGreeting('mark', name => `안녕 ${name}`;)
전통적인 반복문인 for문과 for..of 문은 간결함, 가독성, 예측 가능성을 충족시키지 못하는 경우가 많다. 배열 메서드를 사용하면 이런 목표를 충족시킬 수 있다.
map()
형태를 바꿀 수 있지만 길이는 유지된다.
ex) 전체 팀원의 이름을 가져온다.
sort()
형태나 길이는 변경되지 않고 순서만 변한다.
ex) 팀원 이름을 알파벳순으로 정렬한다.
filter()
길이는 변경하지만 형태는 바꾸지 않는다.
ex) 개발자만 선택한다.
find()
배열을 반환하지 않는다. 한 개의 데이터가 반환되고 형태는 바뀌지 않는다.
ex) 팀의 관리자를 찾는다.
forEach()
형태를 이용하지만 아무것도 반환하지 않는다.
ex) 모든 팀원에게 상여를 지급한다.
reduce()
길이와 형태를 바꾸는 것을 비롯해 무엇이든 처리할 수 있다.
ex) 개발자와 개발자가 아닌 모든 팀원의 수를 계산한다.
for문을 사용하여 숫자로 된 가격만을 다시 배열에 담는 과정은 다음처럼 길고 예측 불가능하다.
const prices = ['1,0', '흥정 가능', '2.15'];
const formattedPirces = [];
for (let i=0; i<prices.length; i++) {
const price = parseFloat(prices[i]);
if (price) {
formattedPrices.push(price);
}
}
배열 메서드를 사용하면 다음과 같이 예측 가능하며 코드의 수를 줄일 수 있다.
const prices = ['1,0', '흥정 가능', '2.15'];
const formattedPirces = price.map(price => parseFloat(price)).filter(price => price);