Overview

JavaScript 에서는 마지막 부분을 잘라내는 방법 (drop) 이 여러 가지 있습니다.

그 중에서 가장 일반적으로 사용하는 게 substringslice 인데 둘의 사용법과 차이점을 알아봅니다.


1. substring

string.substring(start, end);

substring 은 이름 그대로 문자열의 일부를 구하는 함수이며 사용법은 위와 같습니다.

잘라내고자 하는 문자열의 시작 (start) 과 끝 (end) 인덱스를 입력합니다.

가장 헷갈릴 만한 점은 start 는 자르는 대상에 포함되고 end 는 포함되지 않습니다.


Example

// 마지막 문자 n 개 버리기
string.substring(0, string.length - n);

마지막 문자들만 버릴 예정이므로 start 는 무조건 0 으로 하고 자를 문자의 갯수만큼 n 을 입력하면 됩니다.


2. slice

string.slice(start, end);

slicesubstring 과 사용법과 문법이 완전히 같습니다.

하지만 단 하나의 차이가 있다면 파라미터로 음수값을 넘길 수 있다는 점입니다.

음수값은 쉽게 이해하자면 -n == string.length - n 으로 생각하면 됩니다.


Example

// 마지막 문자 n 개 버리기
string.slice(0, -n);

음수를 사용할 수 있다는 점 때문에 substring 보다 훨씬 간결합니다.


Conclusion

사용법은 비슷하지만 음수 파라미터의 사용이 가능한 slice 가 훨씬 사용하기 편한 것 같습니다.

StackOverflow 에서는 substring 이 속도가 더 빠르다는 결과도 있었던 것 같은데, 과거의 이야기고 현재는 비슷하다고 하네요.

실제로 벤치마크 가능한 사이트에서 slice vs substr vs substring 을 테스트 해보면 비슷하게 나옵니다.


Reference

Set

JavaScript 에서 Set 자료 구조는 ES6 에서 추가되었습니다.

Set 은 중복을 허용하지 않고 순서가 없는 리스트입니다.


생성자 Constructor

const a = new Set()
// Set { }

const b = new Set([1, 2, 3])
// Set { 1, 2, 3 }

const c = new Set([1, 1, 1])
// Set { 1 }

add

값을 추가합니다.

값을 추가할 때 Object.is() 메서드를 사용해서 값을 비교합니다.

const a = new Set()
// Set { }

a.add(1)
// Set { 1 }

a.add(2)
// Set { 1, 2 }

a.add(1)
// Set { 1, 2 }

size

크기를 알려줍니다.

size() 가 아니라 size 입니다.

const a = new Set([1, 2, 3])
// Set { 1, 2, 3 }

a.size
// 3

has

값이 이미 있는 지 검사합니다.

const a = new Set([1])
// Set { 1 }

a.has(1)
// true

a.has(2)
// false

delete

값을 지웁니다.

만약 값이 있어서 지우는데 성공하면 true 를 리턴하고 아니면 false 를 리턴합니다.

const a = new Set[1, 2, 3])
// Set { 1, 2, 3 }

a.delete(1)
// true
// Set { 2, 3 }

a.delete(4)
// false
// Set { 2, 3 }

clear

Set 에 있는 모든 값을 지웁니다.

const a = new Set([1, 2, 3])
// Set { 1, 2, 3 }

a.clear()
// Set { }

forEach

forEach 를 사용하여 Set 을 순회할 수 있습니다.

forEach 는 콜백 함수를 파라미터로 받으며 콜백 함수는 세 가지 파라미터를 받습니다.

  1. 키 (index)
  2. 현재 배열 (여기서는 Set)

Set 은 키값이 따로 없기 때문에 1번 2번이 같은 값을 가집니다.

const a = new Set([1, 2, 3, 4, 5])
// Set { 1, 2, 3, 4, 5 }

a.forEach((value) => {
    console.log(value)
})
// 1
// 2
// 3
// 4
// 5

a.forEach((key, value) => {
    console.log(key, value)
})
// 1 1
// 2 2
// 3 3
// 4 4
// 5 5

a.forEach((key, value, currentSet) => {
    console.log(key value, currentSet)
})
// 1 1 Set { 1, 2, 3, 4, 5 }
// 2 2 Set { 1, 2, 3, 4, 5 }
// 3 3 Set { 1, 2, 3, 4, 5 }
// 4 4 Set { 1, 2, 3, 4, 5 }
// 5 5 Set { 1, 2, 3, 4, 5 }

Set ⇒ Array 또는 Array ⇒ Set

전개 연산자 (spread) 를 사용하면 간단하게 Set 을 Array 로, Array 를 Set 으로 변경할 수 있습니다.

const a = new Set([1, 2, 3]) 
// Array => Set { 1, 2, 3}

const b = [...a]
// Set => [1, 2, 3]

문자열 한글 포함 여부 확인

정규식을 사용하면 한글의 포함 여부를 알 수 있습니다.

const koRegex = /[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]/;

koRegex.check("hello world"); // false
koRegex.test("안녕"); // true
koRegex.test("반가워 Hi"); // true
koRegex.test("ㅏㅏㅁㄹㄹㄲ"); // true

템플릿 리터럴

템플릿 리터럴 (template literals)은 변수를 이용해서 동적으로 문자열을 생성할 수 있다.

기존에는 문자열에 변수를 추가하려면 더하기 기호를 반복해서 사용해야했다.

const year = 22;
const name = 'alice';
const text = 'Hi! My name is ' + name + ' and I am ' + year * 2 + ' years old';
console.log(text);      // Hi! My name is alice and I am 22 years old


이런식으로 코드를 작성할 경우 시간도 너무 오래걸리고 따옴표로 인한 가독성도 굉장히 떨어지게 된다.

ES6 에서는 템플릿 리터럴로 바꾸어 표현할 수 있다.

템플릿 리터럴은 백틱(` `) 을 사용하며 변수나 식은 ${variable} ${expression} 으로 입력한다.

const text = `Hi! My name is ${name} and I am ${year * 2} years old`


여러 줄을 입력할 때는 엔터를 입력하면 된다.

// 기존: \n 을 사용
const text = 'Hi! My name is ' + name + '\n and I am ' + year * 2 + ' years old';

// ES6: 엔터 추가
const text = `Hi! My name is ${name} 
and I am ${year * 2} years old`


async await

Promise 를 사용해도 어쩔 수 없는 한계가 존재한다.

콜백 함수에 비해서 조금 나아졌다고는 하나 중첩해서 쓰거나 return 으로 함수 호출하면서 로직이 복잡해지면 가독성이 떨어진다.

async await 을 사용하면 then 체이닝보다 가독성이 좋아진다.

하지만 async await 도 프로미스를 활용하는 개념이기 때문에 프로미스를 완전히 대체할 수는 없다.


1. 사용하기

1.1. async

async 키워드를 이용해서 정의된 함수는 항상 프로미스를 반환한다.

따라서 함수 호출 뒤에 then 으로 체이닝이 가능하다.

리턴값이 프로미스라면 그대로 반환된다.

async function getData1() {
  return 111;
}

const getData2 = async () => {
  throw new Error(222);
}

async function getData3() {
  return Promise.resolve(333);
}

getData1().then(data => console.log(data));     // 111
getData2().catch(err => console.log('getData2: ' + err));   // getData2: Error: 222
getData3().then(data => console.log(data));     // 333


1.2. await

await 키워드는 async 함수 내부에서만 사용된다.

await 키워드를 붙여서 프로미스를 사용하면 해당 프로미스가 처리됨 상태가 될 때까지 기다린다.

function printAfter(seconds) {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log('print ' + seconds);
      resolve(seconds);
    }, seconds * 1000)
  },);
}

async function log() {
  const data1 = await printAfter(3);    // 3 초 뒤 출력
  const data2 = await printAfter(5);    // 8 초 뒤 출력
  console.log(data1, data2);
}

log();


2. 활용하기

2.1. 비동기 함수 병렬 실행

await 키워드가 붙으면 해당 함수가 끝날때까지 기다리게 된다.

하지만 여러 개의 await 함수를 호출할 때 함수들 간의 의존성이 없다면 굳이 순차적으로 실행할 필요가 없다.

프로미스는 생성과 동시에 실행된다는 점을 활용하여 프로미스 생성을 먼저 하고 await 키워드를 나중에 호출하는 방법을 사용하면 된다.

async function log() {
  const p3 = printAfter(3);     // 3초 뒤 출력
  const p5 = printAfter(5);     // 5초 뒤 출력

  const data1 = await p3;
  const data2 = await p5;
  
  console.log(data1, data2);
}


아니면 Promise.all 을 사용한다면 더 간단하게 표현 할 수 있다.

async function log() {
  const [data1, data2] = await Promise.all([printAfter(3), printAfter(5)]);

  console.log(data1, data2);
}


2.2. 예외 처리

try catch 문으로 감싸면 async / sync 함수 구분하지 않고 발생하는 예외들을 모두 잡는다.

async function log() {
  try {
    const data = printAfter(3);
    console.log(data);
  } catch (error) {
    console.log(error);
  }
}


프로미스 (Promise)

프로미스는 비동기 상태를 값으로 다룰 수 있는 객체다.

프로미스가 사용되기 전에는 콜백 패턴이 많이 사용되었다.

그러다가 여러 가지 프로미스 라이브러리가 등장하면서 널리 사용되었고 ES6 에서는 프로미스가 자바스크립트 언어에 포함되었다.


1. 콜백(Callback) 패턴의 문제

자바스크립트에서는 비동기 프로그래밍의 한 가지 방법으로 콜백(callback) 패턴을 많이 사용했다.

하지만 콜백 패턴은 조금만 중첩되어도 코드가 복잡해지는 콜백 지옥(Callback Hell)을 만들어낸다.

function display(data) {
  console.log("result: " + data);
}

function conactData(data) {
  parsedData = data.concat(" parsing");
  console.log(parsedData);
  display(parsedData);
}

function getData(callback) {
  data = "This is new Data";
  console.log(data);
  callback(data, display);
}

getData(parsing);
// This is new Data
// This is new Data parsing
// result: This is new Data parsing


위 코드는 프로미스를 사용하면 아래와 같이 좀더 보기 쉽게 바꿀 수 있다.

const getData = new Promise((resolve, reject) => {
  data = "This is new Data";
  console.log(data);
  resolve(data);
})

getData
  .then(data => {
    // concatData
    parsedData = data.concat(" parsing");
    console.log(parsedData);
    return parsedData;
  })
  .then(data => {
    // display
    console.log("result: " + data);
    return data;
  })
// This is new Data
// This is new Data parsing
// result: This is new Data parsing


2. 프로미스의 세가지 상태

프로미스는 다음 세 가지 상태 중 하나로 존재한다.

상태설명
대기 중 (pending)결과를 기다리는중
이행됨 (fulfilled)수행이 정상적으로 끝났고 결과값을 갖고 있음
거부됨 (rejected)수행이 비정상적으로 끝났음

그리고 이행됨, 거부됨 상태를 처리됨(settled) 상태라고 한다.

프로미스는 처리됨 상태가 되면 더 이상 다른 상태로 변경되지 않는다.

대기 중 상태일때만 이행됨 또는 거부됨 상태로 변할 수 있다.


3. 프로미스 생성

3.1. new 키워드로 생성

// 1. new 키워드로 생성
const promise = new Promise((resolve, reject) => {
  // ..
  // resolve() or reject('error')
})

new 키워드로 생성한 프로미스는 대기 중 상태가 된다.

생성자에 입력되는 resolve 와 reject 는 콜백 함수이다.

resolve 를 호출하면 promise 는 이행됨 상태가 된다.

반대로 reject 를 호출하면 거부됨 상태가 된다.

만약 생성자에 입력된 함수 안에서 예외가 발생하면 거부됨 상태가 된다.

new 키워드로 생성된 프로미스의 내부는 즉시 실행된다.

만약 API 요청을 보내는 비동기 코드가 있다면 프로미스가 생성되는 순간에 요청을 보낸다.


3.2. Promise.reject 생성

// Promise.reject 로 생성
const promise = Promise.reject('error');

거부됨 상태인 프로미스를 생성한다.


3.3. Promise.resolve 생성

// Promise.resolve 로 생성
const promise = Promise.resolve(params);

const p1 = Promise.resolve(123);
const p2 = Promise.resolve(p1);   // return p1
console.log(p2 === p1);           // true

입력값이 프로미스라면 그 객체가 그대로 반환되고, 프로미스가 아니라면 이행됨 상태인 프로미스가 반환된다.


4. 프로미스 사용

4.1. then

then 은 처리됨 상태가 된 프로미스를 처리할 때 사용되는 메소드다.

프로미스가 처리됨 상태가 되면 then 메소드의 파라미터로 전달된 함수가 호출된다.

then 메소드는 항상 프로미스를 반환하기 때문에 하나의 프로미스에서 연속적으로 then 메소드를 호출할 수 있다.

getData().then(onResolve, onReject);


만약에 함수 수행 중에 예외가 발생해서 거부됨 상태가 되면 onReject 함수가 존재하는 then 까지 이동한다.

onReject 함수가 실행되고 나면 프로미스는 다시 이행됨 상태가 되어 4 다음에는 5 를 출력한다.

// print: 4 5
Promise.reject('error')
  .then(() => console.log('1'))
  .then(() => console.log('2'))
  .then(() => console.log('3'), () => console.log('4'))
  .then(() => console.log('5'));


4.2. catch

catch 는 프로미스 수행 중에 발생한 예외를 처리한다.

then 함수의 onReject 와 같은 역할을 한다.

예외 처리는 then 보다 catch 함수를 사용하는 게 가독성이 더 좋다.

Promise.reject('error').then(null, error => {
  console.log(error);
});

Promise.reject('error').catch(error => {
  console.log(error);
})


then 은 onResolve 에서 에러가 발생했을 때 같은 함수 내에 있는 onReject 함수에서 처리할 수 없다.

하지만 catch 를 사용하면 처리가능하고 함수도 좀 더 직관적이다.

catch 도 마찬가지로 프로미스를 반환하기 때문에 계속해서 체이닝을 이어나갈 수 있다.

// onResolve 에서 에러가 발생해도 onReject 가 아닌 뒤에서 처리해야됨
Promise.resolve().then(onResolve, onReject);

// print: this is Error: then error
// print: success then 2
Promise.resolve()
  .then(() => {
    throw new Error('then error');
  })
  .catch(error => {
    console.log('this is ' + error);
    return 2;
  })
  .then(data => {
    console.log('success then: ' + data);
  });


4.3. finally

finally 는 프로미스가 처리됨(settled) 상태일 때 호출되는 함수이다.

프로미스 체인의 가장 마지막에 사용된다.

finally 는 이전에 사용된 프로미스를 그대로 반환하기 때문에 처리됨 상태인 프로미스의 데이터를 건드리지 않고 추가 작업을 할 수 있다.

// print: 123
// print: finish promise
Promise.resolve(123)
  .then(data => {
    console.log(data);
    return data;
  })
  .catch(error => {
    console.log(error);
    return error;
  })
  .finally(() => {
    console.log('finish promise');
  });


5. 프로미스 활용

다음 예제를 통해 Promise 를 활용하는 방법들을 알아본다.

const getData1 = Promise.resolve(1);
const getData2 = new Promise((resolve, reject) => 
  setTimeout(function() {
    console.log('3');
    resolve(2);
  }, 5000));


5.1. Promise.all: 병렬 처리

then 함수를 체인으로 연결하면 각각의 비동기 로직이 병렬로 처리되지 않고 순차적으로 실행된다.

서로 간의 의존성이 없다면 병렬로 처리하는 게 더 빠르다.

Promise.all 함수는 여러 개의 프로미스를 동시에 실행하며 프로미스가 모두 처리되면 처리됨 상태가 되고 하나라도 거부된다면 거부됨 상태가 된다.

// print: 3   (5초 뒤 출력)
// print: 1 2
Promise
  .all([getData1, getData2])
  .then(([data1, data2]) => {
    console.log(data1, data2);
  });


5.2. Promise.race: 가장 빨리 처리된 프로미스

Promise.race 는 여러 개의 프로미스 중 가장 빨리 처리된 프로미스를 반환한다.

여러 개의 프로미스 중 하나라도 처리되면 처리됨 상태가 된다.

프로미스 간의 격차가 커서 먼저 Promise.race 가 실행 되더라도 다른 프로미스의 작업은 중지되지 않는다.

// print: 1
// print: 3   (5초 뒤 출력)
Promise
  .race([getData1, getData2])
  .then(data => {
    console.log(data);
  });


강화된 함수의 기능

ES6 에서는 함수의 기능을 온전하게 완성했다고 볼 수 있다.


1. 매개변수에 추가된 기능

1.1. 매개변수 기본값 (default parameter)

ES6 부터 함수 매개변수에 기본값을 줄 수 있다.

function ex(a = 1) {
    console.log({ a });
}

ex();      // { a: 1 }


기본값으로 함수 호출을 넣어줄 수도 있다.

function getDefault() {
    return 1;
}

function ex(a = getDefault()) {
    console.log({ a });
}

ex();       // { a: 1 }


1.2. 나머지 매개변수 (rest parameter)

입력된 매개변수 중에서 특정 매개변수 외의 나머지는 배열로 만들어줄 수 있다.

매개변수 개수가 가변적일 때 유용하다.

function ex(a, ...rest) { console.log({ a, rest }); } ex(1, 2, 3); // { a: 1, rest: [2, 3] }


1.3. 명명된 매개변수 (named parameter)

객체 비구조화를 이용하여 매개변수의 이름을 명시적으로 사용하며 함수를 호출할 수 있다.

매개변수의 이름과 값을 동시에 적을 수 있기 때문에 가독성이 높다.

function getValues1(numbers, greaterThan, lessThan) {
    console.log({ numbers, greaterThan, lessThan });
}

function getValues2({ numbers, greaterThan, lessThan }) {
    console.log({ numbers, greaterThan, lessThan });
}

const numbers = [10, 20, 30, 40];

getValues1(numbers, 5, 25);                             // { numbers, greaterThan: 5, lessThan: 25 }
getValues2({ numbers, greaterThan: 5, lessThan: 25 });  // { numbers, greaterThan: 5, lessThan: 25 }


1.4. 선택적 매개변수 (optional parameter)

명명된 매개변수를 응용하면 선택적 매개변수를 사용할 수도 있다.

getValues1 함수에서는 필요없는 매개변수가 있어도 undefined 로 값을 넣어주어야 한다.

매개변수의 값이 많아지면 일일히 undefined 를 넣어주어야 하고 가독성도 굉장히 떨어지게 된다.

하지만 getValues2 함수에서는 필요없는 매개변수는 코드로 적지 않고 필요한 매개변수만 넣어주면 된다.

getValues1(numbers, undefined, 25);
getValues2({ numbers, greaterThan: 5 });
getValues2({ numbers, lessThan: 25 });


2. 화살표 함수 (arrow function)

ES6 에서는 화살표를 이용하여 함수를 정의하는 방법이 새로 추가되었다.

화살표 함수를 사용하면 함수를 간결하게 작성할 수 있다.


2.1. 한줄 사용

화살표 함수는 한줄로도 간단하게 정의할 수 있다.

중괄호 블록을 사용하지 않고 바로 오른쪽에 정의하며, return 키워드를 명시적으로 정의하지 않아도 오른쪽에 있는 값이 리턴된다.

매개변수가 하나라면 소괄호도 생략 가능하다.

반환하는 값이 Object 라면 반드시 소괄호로 감싸야 한다.

const add = (a, b) => a + b;
const add5 = a => a + 5;                                        // 매개변수가 하나면 소괄호를 생략 가능하다.
const addAndReturnObject = (a, b) => ({ result: a+b });         // 반환값이 Object 라면 소괄호로 감싸준다.
const print = () => console.log("print");


2.2. 여러줄 사용

화살표 함수에 코드가 여러줄이라면 전체를 중괄호로 묶고 return 키워드를 사용한다.

const add = (a, b) => {
    if (a <= 0 || b <= 0) {
        throw new Error('must be positive number');
    }
    return a + b;
}


2.3. this 와 arguments 가 바인딩 되지 않음

화살표 함수에서는 this 와 arguments 가 바인딩 되지 않는다.

만약 arguments 가 필요하다면 나머지 매개변수 (rest parameter) 를 사용한다.

const print = (...rest) => console.log(rest);
print(1, 2);        // [1, 2]


2.4. this 바인딩 차이점

일반 함수는 호출되었을 때, 호출한 대상에 바인딩된다.

아래 코드를 통해 this 가 각각 어디를 바라보고 있는지 알 수 있다.

앞에 f. 를 붙여서 호출하면 this 가 func() 함수를 가리키고 있다.

하지만 다른 변수에 할당한 다음에 아무것도 붙이지 않고 호출하면 this 는 전역객체를 참조한다. (브라우저에서는 window)

function func() {
  this.value = 1;

  this.increase = function() {
    this.value++;
  };

  this.print = function() {
    console.log(this);
  };
}

const f = new func();
f.increase();
console.log(f.value);       // 2
f.print();                  // func { value: 2, increase: ƒ, print: ƒ }

const inc = f.increase;
const print = f.print;
inc();
console.log(f.value);       // 2
print();                    // Window { parent: Window, ... }


기존 ES5 에서는 이런 문제점을 우회하기 위해 클로저(closure) 라는 개념을 사용했다.

function func() {
  this.value = 1;
  that = this;
  
  this.increase = function() {
    that.value++;
  };

  this.print = function() {
    console.log(that);
  };
}


일반 함수는 호출할 당시의 객체에 this 바인딩 되는 대신 화살표 함수는 가장 가까운 일반 함수를 참조한다.

따라서 함수를 어디에 재할당 하던지 항상 생성되었을 당시의 일반함수를 참조하게 된다.

function func() {
  this.value = 1;

  this.increase = () => {
    this.value++;
  };

  this.print = () => {
    console.log(this);
  };
}

const f = new func();
f.increase();
console.log(f.value);       // 2
f.print();                  // func { value: 2, increase: ƒ, print: ƒ }

const inc = f.increase;
const print = f.print;
inc();
console.log(f.value);       // 3
print();                    // func { value: 2, increase: ƒ, print: ƒ }


객체와 배열의 사용성 개선

1. 간편한 생성 및 수정

1.1. 단축 속성명

단축 속성명 (shorthand property names) 로 객체 리터럴 코드를 간편하게 작성할 수 있다.

const name = 'alice';
const obj = {
    age: 21,
    name,
    getName() { return this.name; },
}
console.log(obj);   // { age: 21, name: "alice", getName: ƒ getName() }

새로 만드려는 객체의 속성명이 이미 변수로 존재하면 변수 이름만 적어주면 된다.

이때 속성명은 변수 이름과 같아진다.

속성값이 함수이면 function 키워드 없이 함수명만 적으면 된다.

마찬가지로 속성명은 함수명과 같아진다.


1.2. 계산된 속성명

계산된 속성명 (computed property names) 으로 객체의 속성명을 동적으로 결정할 수 있다.

function create1(key, value) {
  const obj = {};
  obj[key] = value;
  return obj;
}

create2 = (key, value) {}

function create2(key, value) {
  return { [key]: value };
}

console.log(create1('key1', 'value1'));       // { key1: 'value1' }
console.log(create2('key2', 'value2'));       // { key2: 'value2' }

계산된 속성명을 사용하면 create2 처럼 간결하게 코드를 짤 수 있다.

key 를 대괄호 [ ] 로 감싸는 이유는 return { key: value } 처럼 하면 속성명으로 변수값이 아닌 key 자체가 되어버리기 때문이다.


2. 속성값 간편하게 가져오기

2.1. 전개 연산자

전개 연산자(spread operator)는 배열이나 객체의 모든 속성을 풀어놓을 때 사용한다.


2.1.1. 매개변수 여러개 전달

코드의 첫 번째 줄처럼 전개 연산자를 사용하지 않는다면 매개변수의 갯수가 4 개로 고정된다.

하지만 전개 연산자를 사용한다면 numbers 배열의 갯수가 몇개든 전부 전달할 수 있다.

Math.max(1, 2, 3, 4);

const numbers = [1, 2, 3, 4];
Math.max(...numbers);


2.1.2. 배열과 객체 복사

전개 연산자를 이용하여 간단하게 배열과 객체를 복사할 수 있다.

복사된 배열이나 객체는 새로운 값이기 대문에 수정해도 기존 배열이나 객체에 영향을 주지 않는다.

배열의 경우 전개 연산자를 사용하면 그 순서가 유지된다.

const arr1 = [1, 2, 3];
const obj1 = { age: 23, name: 'alice' };

const arr2 = [...arr1];
const obj2 = { ...obj1 };
arr2.push(4);
obj2.age = 80;

console.log(arr1);      // [1, 2, 3]
console.log(arr2);      // [1, 2, 3, 4]
console.log(obj1);      // { age: 23, name: 'alice' }
console.log(obj2);      // { age: 80, name: 'alice' }


전개 연산자를 사용하면 서로 다른 두 배열이나 객체를 쉽게 합칠 수 있다.

const obj1 = { age: 21, name: 'alice' };
const obj2 = { address: 'seoul' };
const obj3 = { ...obj1, ...obj2 };      // { age: 21, name: 'alice', address: 'seoul' }

const arr1 = [1, 3, 5];
const arr2 = [2, 4, 6];
const arr3 = [...arr1, ...arr2];        // [1, 3, 5, 2, 4, 6]


ES5 까지는 중복된 속성명으로 합치면 에러가 발생했지만 ES6 부터는 허용된다.

마지막에 입력된 값이 최종값이 된다.

const obj1 = { age: 21, name: 'alice' };
const obj2 = { name: 'bob' };
const obj3 = { ...obj1, ...obj2 };      // { age: 21, name: 'bob' }
const obj4 = { ...obj2, ...obj1 };      // { name: 'alice', age: 21 }


2.2. 비구조화

2.2.1. 배열 비구조화

배열 비구조화(array destructuring)를 사용하면 배열의 여러 속성값을 변수로 쉽게 할당할 수 있다.

배열의 속성값이 왼쪽 변수에 순서대로 들어간다.

const arr = [1, 2];
const [a, b] = arr;     // a: 1, b: 2


배열 비구조화 정의 시 기본값을 설정할 수 있다.

만약 기본값도 없고 할당되는 값도 없다면 undefined 가 된다.

const arr = [1];
const [a = 10, b = 20] = arr;       // a: 1, b: 20


두 값을 교환할 수도 있다.

let a = 1;
let b = 2;
[a, b] = [b, a];        // a: 2, b: 1


일부 속성값을 무시할 수도 있다.

const arr = [1, 2, 3];
const [a, , c] = arr;       // a: 1, c: 3


나머지 값을 별도의 배열로 만들 수도 있다.

const arr = [1, 2, 3];
const [first, ...rest] = arr;       // first: 1, rest: [2, 3]
const [a, b, c, ...empty] = arr;    // a: 1, b: 2, c: 3, empty: []


2.2.2. 객체 비구조화

객체 비구조화(object destructuring)를 사용하면 여러 속성값을 변수로 쉽게 할당할 수 있다.

순서대로 들어가는 배열과 달리 Object 의 키에 맞춰서 들어간다.

그리고 Object 에 존재하는 키와 동일한 이름의 변수명을 사용해야 한다.

const obj = { age: 21, name: 'alice' };
const { age, name } = obj;      // age: 21, name: 'alice'
const { name, age } = obj;      // age: 21, name: 'alice'
const { a, b } = obj;           // a: undefined, b: undefined


임의로 다른 변수명에 할당할 수도 있다.

const obj = { age: 21, name: 'alice' };
const { age: age2, name } = obj;        // age: not defined error, age2: 21, name: 'alice'


기본값을 정의할 수 있다.

들어오는 값이 undefined 인 경우에만 기본값으로 세팅된다.

기본값 세팅과 다른 변수명에 할당을 동시에 사용할 수도 있다.

const obj = { age: undefined, name: null, grade: 'A' };
const { age: age2 = 0, name = 'noName', grade = 'F', address = 'seoul' } = obj;  
// age: not defined error, age2: 0, name: null, grade: 'A', address: 'seoul'


변수 정의: const, let

ES5 까지는 var 키워드로 변수를 정의했다.

ES6 에서는 const 와 let 을 이용하는 새로운 변수 정의 방법이 생겼다.


1. var 문제점

1.1. 함수 스코프

스코프(scope) 란 변수가 사용될 수 있는 영역을 말한다.

var 는 함수 스코프라서 함수 밖에서 사용하면 에러가 발생한다.

function error() {
    var a = 1;
}
console.log(a);     // ReferenceError: a is not defined


var 키워드 없이 변수를 선언하면 전역 변수가 된다.

function global() {
    a = 1;
}
function local() {
    console.log(a);
}
global();
local();    // 1 출력


for 문에서 선언된 변수가 사라지지 않는다.

for 문 뿐만 아니라 while, switch, if 문 등 함수 내부에서 작성되는 모든 코드가 같은 문제를 안고 있다.

for (var i = 0; i < 10; i++) {
    console.log(i);     // 0 ~ 9 출력
}
console.log(i);     // 10 출력


1.2. 호이스팅(hoisting)

var 로 정의된 변수는 해당 스코프의 최상단으로 끌어올려진다.

변수를 선언 없이 사용하면 참조 에러가 발생한다.

console.log(a);     // ReferenceError: a is not defined


하지만 다음 코드는 에러가 발생하지 않는다.

console.log(a);     // undefined
var a = 1;

호이스팅에 의해 위 코드는 아래와 같이 취급된다.

var a = undefined;
console.log(a);     // undefined
a = 1;


심지어 변수 선언 전에 값을 할당할 수도 있다.

console.log(a);     // undefined
a = 2;
console.log(a);     // 2
var a = 1;

이런 호이스팅 개념은 코드를 직관적이지 않게 만들며, 버그가 발생하여도 정확한 원인을 찾기 힘들게 만든다.


1.3. 항상 재할당 가능

var 는 재할당 가능한 변수로만 만들 수 있다.

따라서 상수처럼 고정된 값을 변수로 선언해서 이용해야 할 때에도 변경될 위험이 존재하고 있다.


2. var 문제점 해결: const, let

2.1. 블록 스코프

대부분의 언어에서 블록 스코프를 사용한다.

블록 스코프에서는 블록을 벗어나면 변수를 사용할 수 없다.

블록을 벗어나도 살아있던 var 변수와 달리 const, let 변수는 블록을 벗어나면 관리해줄 필요가 없다.

if (true) {
    const a = 0;
}
console.log(a);     // ReferenceError: a is not defined


2.2. 호이스팅(hoisting)

const, let 도 호이스팅 된다.

하지만 변수를 선언하기 전에 사용하려고 하면 에러가 발생한다.

console.log(a);      // ReferenceError: Cannot access 'a' before initialization
const a = 1;

이 때문에 const, let 은 호이스팅 되지 않는다고 생각하기 쉽다.

const, let 은 서로 다른 스코프에서 같은 이름의 변수를 사용할 때 실수를 방지해준다.


아래 코드에서 a 는 다른 블록 스코프지만 상위에 선언되었기 때문에 1 이 출력된다.

const a = 1;
{
    console.log(a);     // 1
}


하지만 아래 코드에서는 참조 에러가 발생한다.

하위 블록 스코프에서 호이스팅으로 인해 const a = 2 로 할당 되기 전까지는 사용이 불가능하다.

이처럼 같은 이름의 변수를 사용할 때의 실수를 방지해준다.

const a = 1;
{
    console.log(a);
    const a = 2;
}


2.3. 재할당 불가능 const

const 로 정의된 변수는 재할당이 불가능하다.

let 으로 선언된 변수는 재할당 가능하다.

대신 const 로 정의된 객체의 내부 속성값은 수정 가능하다.

const arr = [1, 2];
arr[0] = 3;
arr.push(4);
console.log(arr);   // [3, 2, 4]


+ Recent posts