
이 정리본은
ES6를 중심으로, 강의 흐름에서 함께 다룬 현대JavaScript문법까지 초보자 기준으로 다시 풀어쓴 최종본이다.
각 문법은 단순히 뜻만 외우는 것이 아니라, 왜 필요한지와 코드에서 어떻게 쓰이는지까지 연결해서 이해하는 것을 목표로 한다.
엄밀히 말하면 이 글의 모든 문법이ES6자체에만 포함되는 것은 아니다.
async,await,Promise.allSettled(),Promise.any(),Promise.prototype.finally(), 객체Rest/Spread,#비공개 프로퍼티처럼 이후 표준에서 추가된 문법도 함께 다룬다.
또forEach(),map(),reduce(),filter()처럼ES6이전부터 사용되던 배열 메서드도 현대JavaScript코드에서 자주 함께 쓰이므로 같이 정리한다.
따라서 이 글은ES6에만 들어 있는 문법 목록을 외우는 글이 아니라,ES6를 기준으로 현대JavaScript에서 자주 이어지는 문법 흐름을 함께 이해하는 정리본이다.
ES6를 이해하려면 먼저ECMAScript와JavaScript의 관계를 구분해야 한다.
ECMAScript는JavaScript가 따라야 하는 표준 규칙이다.
여기서 표준 규칙은 문법을 어떻게 써야 하는지, 변수와 함수 같은 기능이 어떤 방식으로 동작해야 하는지를 정해 둔 기준이다.
JavaScript는 실제로 우리가 웹 페이지나 프로그램에서 사용하는 언어이다.
버튼을 눌렀을 때 동작하게 만들고, 화면 내용을 바꾸고, 서버에서 데이터를 받아 처리할 때 사용한다.
이때JavaScript가 사용하는 문법과 동작 방식의 기준이 바로ECMAScript이다.
ECMAScript는 표준이고,JavaScript는 그 표준을 바탕으로 실제로 사용하는 언어이다.
ECMAScript는JavaScript의 기준이 되는 표준 문법이고,ES6는 그 표준 중 현대적인JavaScript문법을 이해할 때 중요한 기준이 되는 버전이다.
ECMAScript는 시간이 지나면서 계속 보완되었고,ES6는 현대적인JavaScript문법을 이해할 때 중요한 기준이 된다.
ES6는ECMAScript 6를 줄여 부르는 말이다.
기존JavaScript에서도 코드를 작성할 수는 있었다.
하지만 코드가 길어지면 변수 관리가 어렵고, 문자열 연결이 불편하고, 함수 작성도 반복적으로 길어지는 경우가 많았다.
ES6는 이런 불편함을 줄이기 위해 여러 문법을 추가했다.
예를 들어let,const는 변수를 더 안전하게 선언하기 위해 사용한다.
템플릿 리터럴은 문자열을 더 읽기 쉽게 작성하기 위해 사용한다.
화살표 함수는 함수를 더 짧게 작성하기 위해 사용한다.
클래스는 객체를 만드는 구조를 더 익숙한 형태로 표현하기 위해 사용한다.
Promise는 비동기 작업의 성공과 실패를 다루기 위해 사용한다.
async,await은ES6자체 문법은 아니지만,Promise기반 비동기 코드를 더 읽기 쉽게 작성하기 위해 함께 이해해야 하는 현대JavaScript문법이다.
즉,ES6는 단순히 새로운 문법을 많이 외우는 구간이 아니다.
기존 방식으로도 할 수 있었던 일을 더 간결하고, 더 읽기 쉽고, 더 실수하기 어렵게 작성하기 위한 문법 흐름이다.
기존 방식과 ES6 방식 비교
아래 예제는 같은 문자열을 출력하지만 작성 방식이 다르다.
기존 방식은var로 변수를 만들고 문자열과 변수를+로 이어 붙인다.
ES6방식은const로 변수를 만들고 템플릿 리터럴을 사용해 한 문장처럼 작성한다.// es6-intro-string-example.js var oldLanguage = "JavaScript"; // 기존 방식의 변수 선언 console.log("기존 방식: 안녕하세요 " + oldLanguage); // +로 문자열 연결 const newLanguage = "JavaScript"; // ES6 방식의 변수 선언 console.log(`ES6 방식: 안녕하세요 ${newLanguage}`); // 템플릿 리터럴로 값 삽입// 출력결과 // 기존 방식: 안녕하세요 JavaScript // ES6 방식: 안녕하세요 JavaScript두 코드는 출력 결과가 같다.
하지만ES6방식은 문자열을 중간에 끊지 않고 문장 안에 값을 자연스럽게 넣는다.
이처럼ES6문법은 완전히 새로운 일을 하기 위한 문법이라기보다, 기존에 하던 일을 더 읽기 쉽게 작성하도록 도와주는 경우가 많다.
ES6와 ECMAScript 핵심 정리
ECMAScript는JavaScript의 표준 규칙이다.JavaScript는 실제로 사용하는 언어이다.ES6는 현대적인JavaScript문법을 이해할 때 중요한 기준이 되는 버전이다.ES6문법은 코드를 더 간결하고 읽기 쉽게 만들기 위해 사용한다.async/await,Promise.allSettled(),Promise.any(),Promise.prototype.finally(), 객체Rest/Spread,#비공개 프로퍼티는ES6이후에 추가된 문법이지만, 현대JavaScript흐름에서 함께 이해하면 좋다.forEach(),map(),reduce(),filter()는ES6자체 문법은 아니지만 배열 처리 흐름을 이해하기 위해 함께 정리한다.
ES6를 배우는 이유는 문법 이름을 외우기 위해서가 아니라,JavaScript코드를 더 안전하고 간결하며 읽기 쉽게 작성하기 위해서이다.
JavaScript에서 값을 사용하려면 먼저 변수를 만들어야 한다.
변수는 값을 저장해 두고, 나중에 다시 꺼내 쓰기 위한 이름 붙은 공간이라고 이해하면 된다.
예를 들어 숫자10을 여러 번 사용해야 한다고 생각해 보자.
그때마다10을 직접 쓰는 대신score라는 이름에10을 저장해 두면, 코드에서score라는 이름으로 값을 다시 사용할 수 있다.
이처럼 변수는 값을 기억해 두기 위한 기본 도구이다.
그런데ES6에서는 변수를 선언하는 방식이 중요해졌다.
기존에는 주로var를 사용했지만,ES6부터는let과const를 함께 사용한다.
이 세 가지는 모두 변수를 만드는 문법이지만, 같은 이름을 다시 만들 수 있는지, 저장된 값을 다시 바꿀 수 있는지, 변수가 어디까지 유효한지가 다르다.
var,let,const의 차이를 제대로 이해하려면 재선언, 재할당,scope를 먼저 구분해야 한다.
이 세 개념을 구분하지 못하면var는 왜 위험한지,let과const는 왜 더 많이 권장되는지 이해하기 어렵다.
재선언과 재할당은 다르다
재선언은 똑같은 이름의 변수를 다시 만드는 것이다.
이미score라는 변수를 만들었는데, 다시var score처럼 같은 이름의 변수를 또 만드는 상황을 말한다.
즉, 변수 이름 자체를 다시 선언하는 것이다.
재할당은 이미 만들어진 변수에 다른 값을 다시 넣는 것이다.
변수 이름을 새로 만드는 것이 아니라, 기존 변수 안의 값만 바꾸는 것이다.
예를 들어score에10이 들어 있었는데, 나중에score = 20처럼 값을 바꾸면 재할당이다.
두 개념은 비슷해 보이지만 다르다.
재선언은 변수를 다시 만드는 것이고, 재할당은 기존 변수의 값을 바꾸는 것이다.
초보자는 이 둘을 같은 것으로 생각하기 쉽지만,let과const를 이해할 때 반드시 나누어 봐야 한다.
차이를 짧게 정리하면 이렇다.
- 재선언은 같은 이름의 변수를 다시 만드는 것이다.
- 재할당은 이미 있는 변수에 다른 값을 다시 넣는 것이다.
- 재선언은 변수 이름을 다시 선언하는 문제이다.
- 재할당은 변수 안의 값이 바뀌는 문제이다.
이 차이를 알아야
var,let,const가 무엇을 허용하고 무엇을 막는지 이해할 수 있다.
변수 유효 범위는 변수를 사용할 수 있는 범위이다
scope는 변수나 함수 같은 이름을 사용할 수 있는 유효 범위이다.
쉽게 말하면 “이 변수 이름을 어디까지 사용할 수 있는가”를 정하는 범위이다.
어떤 변수는 코드 전체에서 사용할 수 있고, 어떤 변수는 특정 블록 안에서만 사용할 수 있다.
여기서 블록은{}로 감싼 코드 영역을 말한다.
scope가 중요한 이유는 변수의 영향 범위를 정하기 때문이다.
변수가 너무 넓은 범위에서 사용되면, 코드가 길어졌을 때 어디에서 값이 바뀌었는지 찾기 어려워진다.
반대로 변수가 필요한 곳에서만 사용되면, 코드 흐름을 더 쉽게 따라갈 수 있다.
즉,scope는 단순히 문법 용어가 아니다.
변수가 영향을 주는 범위를 제한해서 코드 실수를 줄이는 기준이다.
세 가지 변수 선언 방식의 기본 차이
이제
var,let,const를 비교하면 차이가 분명해진다.
세 문법은 모두 변수를 만들 때 사용하지만, 허용하는 동작이 다르다.
var는 재선언과 재할당이 모두 가능하다.
그래서 같은 이름의 변수를 다시 만들어도 오류가 나지 않고, 값도 다시 바꿀 수 있다.
겉으로 보면 편해 보이지만, 코드가 길어질수록 같은 이름을 실수로 다시 선언해도 발견하기 어렵다.
let은 재할당은 가능하지만 재선언은 불가능하다.
즉, 값을 나중에 바꿀 수는 있지만, 같은 이름의 변수를 같은 범위 안에서 다시 만들 수는 없다.
그래서 값이 변해야 하는 변수에는let을 사용할 수 있다.
const는 재선언과 재할당이 모두 불가능하다.
한 번 값을 넣으면 같은 변수에 다른 값을 다시 넣을 수 없다.
그래서 처음 정한 값 자체를 다른 값으로 바꾸면 안 되는 경우에 사용한다.
차이를 정리하면 아래와 같다.
var는 재선언이 가능하다.var는 재할당도 가능하다.let은 재선언이 불가능하다.let은 재할당이 가능하다.const는 재선언이 불가능하다.const는 재할당도 불가능하다.정리하면 기준은 단순하다.
값이 바뀌면let, 값이 바뀌지 않으면const를 사용한다.
그리고 특별한 이유가 없다면var는 피하는 것이 좋다.
var는 너무 유연해서 실수를 막아 주지 못하고,let과const는 잘못된 재선언이나 불필요한 값 변경을 줄여 준다.
기본 예제로 차이 확인하기
아래 예제는
var,let,const의 차이를 가장 기본적인 흐름으로 비교한다.// variable-declaration-basic.js var oldScore = 10; // var로 변수 선언 oldScore = 20; // var는 재할당 가능 var oldScore = 30; // var는 같은 이름으로 재선언 가능 console.log(oldScore); // 마지막 값 출력 let changeScore = 10; // let으로 변수 선언 changeScore = 20; // let은 재할당 가능 console.log(changeScore); // 바뀐 값 출력 const fixedScore = 10; // const로 변수 선언 console.log(fixedScore); // 처음 저장한 값 출력// 출력결과 // 30 // 20 // 10첫 번째 부분에서
oldScore는var로 선언했다.
처음에는10을 넣었고, 그다음20으로 값을 바꾸었다.
그리고 다시var oldScore = 30으로 같은 이름의 변수를 재선언했다.
var는 이것을 허용하기 때문에 마지막에는30이 출력된다.
두 번째 부분에서changeScore는let으로 선언했다.
let은 값을 다시 넣는 재할당은 가능하다.
그래서10으로 시작한 값이20으로 바뀌고, 마지막에는20이 출력된다.
세 번째 부분에서fixedScore는const로 선언했다.
const는 한 번 값을 정하면 같은 변수에 다른 값을 다시 넣을 수 없다.
예제에서는 값을 바꾸지 않았기 때문에 처음 저장한10이 그대로 출력된다.
const는 재할당을 막지만 객체 내부 변경까지 막지는 않는다
const를 처음 배울 때 가장 많이 오해하는 부분이 있다.
const는 “값이 절대 바뀌지 않는다”라고만 외우면 객체에서 헷갈릴 수 있다.
정확히는const는 변수에 다른 값을 다시 대입하는 것을 막는다.
즉,const user = ...라고 선언했다면user라는 변수 이름이 다른 값을 가리키도록 다시 바꿀 수 없다.
하지만user가 객체를 가리키고 있다면, 그 객체 안의 프로퍼티를 바꾸는 것은 가능하다.
여기서 프로퍼티는 객체가 가지고 있는 이름과 값의 묶음이다.// const-object-property-change.js const user = { // const로 객체 저장 nickname: "짱구", // 객체의 프로퍼티 age: 5 // 객체의 프로퍼티 }; user.age = 6; // 객체 내부 프로퍼티 변경은 가능 console.log(user); // 변경된 객체 출력// 출력결과 // { nickname: '짱구', age: 6 }이 코드에서
user는const로 선언되었다.
그런데도user.age = 6은 가능하다.
이유는user변수 자체에 새 객체를 다시 넣은 것이 아니라,user가 가리키는 객체 안의age값을 바꾼 것이기 때문이다.
반대로 아래 코드는 오류가 난다.// const-object-reassign-error.js const user = { // const로 객체 저장 nickname: "짱구", // 객체의 프로퍼티 age: 5 // 객체의 프로퍼티 }; user = { nickname: "짱아", age: 1 }; // const 변수에 새 객체 재할당 시도 console.log(user); // 실행되지 않음// 출력결과 // TypeError: Assignment to constant variable.이 코드는
user라는 변수에 완전히 다른 객체를 다시 넣으려고 했다.
const는 재할당을 막기 때문에 오류가 발생한다.
정리하면 아래처럼 이해하면 된다.
const는 변수 이름이 다른 값을 다시 가리키는 것을 막는다.const로 저장한 객체의 프로퍼티 변경은 가능하다.- 객체 자체를 완전히 못 바꾸게 만드는 문법은 아니다.
- 객체 내부 변경까지 막고 싶다면 별도의 방식이 필요하다.
const는 변수 재할당을 막는 문법이지, 객체 내부 프로퍼티 변경을 자동으로 막는 문법은 아니다.
이 차이를 알아야 뒤에서 객체Spread Operator로 새 객체를 만드는 이유도 쉽게 이해할 수 있다.
새 선언 방식이 막아 주는 실수
let과const는 일부 동작을 막는다.
처음에는 이 제한이 불편해 보일 수 있다.
하지만 실제로는 실수를 빨리 발견하게 도와준다.
먼저let은 같은 범위에서 같은 이름의 변수를 다시 만드는 것을 막는다.
아래 코드는 잘못된 예시이다.// variable-let-redeclare-error.js let userName = "kim"; // let으로 변수 선언 let userName = "lee"; // 같은 범위에서 let 재선언은 불가능// 출력결과 // SyntaxError: Identifier 'userName' has already been declared이 예제에서
let userName = "lee"는 문제가 된다.
이미 같은 범위 안에userName이라는 변수가 있는데, 다시let userName으로 같은 이름의 변수를 만들려고 했기 때문이다.
let은 같은 범위 안에서 재선언을 허용하지 않는다.
이번에는const의 재할당 제한을 보면 된다.
아래 코드도 잘못된 예시이다.// variable-const-reassign-error.js const birthYear = 2000; // const로 변수 선언 birthYear = 2001; // const 재할당은 불가능// 출력결과 // TypeError: Assignment to constant variable.
birthYear = 2001은 문제가 된다.
birthYear는const로 선언했기 때문에 값을 다시 바꿀 수 없다.
const는 처음 저장한 값을 유지해야 하는 변수에 사용한다.
그래서 재할당을 시도하면 오류가 난다.
이 제한은 코드를 안전하게 만든다.
같은 이름의 변수를 실수로 다시 만들면 기존 값이 덮여서 예상하지 못한 결과가 나올 수 있다.
바뀌면 안 되는 값이 중간에 바뀌면 오류의 원인을 찾기 어려워진다.
let과const는 이런 실수를 코드 작성 단계에서 빨리 발견하게 도와준다.
기존 선언 방식의 유효 범위 문제
var의 또 다른 문제는scope이다.
var는 함수 레벨scope를 가진다.
함수 레벨scope는 함수 안에서 선언한 변수는 함수 안에서 유효하고, 함수가 아닌{}블록만으로는 새로운 범위가 만들어지지 않는다는 뜻이다.
반면let과const는 블록 레벨scope를 가진다.
블록 레벨scope는{}안에서 선언한 변수가 그 블록 안에서만 유효하다는 뜻이다.
그래서if,for같은 블록 안에서 만든 변수는 블록 밖에서 함부로 사용할 수 없다.
아래 예제로 차이를 보면 더 쉽다.// variable-scope-example.js if (true) { var varMessage = "var 변수"; // var는 블록 밖에서도 접근 가능 let letMessage = "let 변수"; // let은 블록 안에서만 접근 가능 } console.log(varMessage); // var는 출력 가능 console.log(letMessage); // let은 블록 밖에서 접근 불가능// 출력결과 // var 변수 // ReferenceError: letMessage is not defined
varMessage는if블록 안에서 선언했지만, 블록 밖에서도 출력된다.
var는{}블록만으로 범위가 제한되지 않기 때문이다.
이런 특징 때문에 코드가 길어지면 변수가 어디서부터 어디까지 영향을 주는지 헷갈릴 수 있다.
반면letMessage는if블록 안에서만 사용할 수 있다.
블록 밖에서letMessage를 출력하려고 하면 오류가 난다.
처음에는 불편해 보일 수 있지만, 실제로는 변수가 영향을 주는 범위를 좁혀 준다.
그래서 코드가 길어져도 변수가 어디에서 사용되는지 더 쉽게 파악할 수 있다.
기존 선언 방식의 호이스팅 문제
var에는 호이스팅 문제도 있다.
호이스팅은 변수 선언이 실제 코드 위치보다 위로 끌어올려진 것처럼 동작하는 현상이다.
초보자 입장에서는 “아직 변수를 만들기 전인데도 변수 이름을 사용할 수 있는 것처럼 보이는 현상”이라고 이해하면 된다.
var로 선언한 변수는 선언문보다 먼저 사용해도not found가 아니라undefined가 나올 수 있다.
여기서undefined는 값이 아직 정해지지 않았다는 의미이다.
즉, 변수는 있는 것으로 처리되지만 값은 아직 들어가지 않은 상태로 보이는 것이다.// var-hoisting-example.js console.log(score); // 선언보다 먼저 사용 var score = 100; // var 변수 선언과 값 저장 console.log(score); // 값 저장 후 사용// 출력결과 // undefined // 100이 코드는 이상해 보일 수 있다.
score를 선언하기 전에 출력했는데 오류가 아니라undefined가 나온다.
이유는var score선언이 위로 끌어올려진 것처럼 처리되기 때문이다.
하지만 값100까지 같이 올라가는 것은 아니다.
그래서 첫 번째 출력에서는 값이 없는 상태인undefined가 나오고, 두 번째 출력에서는100이 나온다.
이런 동작은 초보자에게 혼란을 줄 수 있다.
코드를 위에서 아래로 읽었을 때 아직 변수를 만들지 않은 것처럼 보이는데도 오류가 바로 나지 않기 때문이다.
그래서var를 사용하면 실수를 늦게 발견할 수 있다.
변수 선언 방식 핵심 정리
변수 선언 방식은 단순 취향 문제가 아니다.
코드가 길어질수록 변수를 안전하게 관리하는 방식이 중요해진다.
사용 기준은 아래처럼 잡으면 된다.
- 기본적으로
const를 먼저 사용한다.- 값이 나중에 바뀌어야 하면
let을 사용한다.- 특별한 이유가 없다면
var는 사용하지 않는다.처음부터
const를 기본으로 사용하고, 값이 바뀌어야 할 때만let을 사용하는 습관을 들이는 것이 좋다.
var는 재선언, 함수 레벨scope, 호이스팅 때문에 예상하지 못한 결과를 만들 수 있으므로 특별한 이유가 없다면 사용을 피하는 것이 좋다.
정리하면var는 재선언과 재할당이 모두 가능하지만 그만큼 실수를 막기 어렵다.
let은 재할당이 필요한 변수에 사용하고, 같은 범위 안의 재선언을 막아 준다.
const는 변수에 다른 값을 다시 대입하지 않을 때 사용하고, 재선언과 재할당을 모두 막아 준다.
다만 객체를const로 선언해도 객체 내부 프로퍼티 변경까지 자동으로 막는 것은 아니다.
let과const는 코드를 불편하게 만드는 문법이 아니라, 변수 사용 범위를 분명하게 하고 실수를 줄이기 위한 안전장치이다.
JavaScript에서 문자열은 글자 데이터를 의미한다.
기존에는 문자열을 만들 때 보통 큰따옴표나 작은따옴표를 사용했다.
문자열만 출력할 때는 이 방식도 충분하다.
하지만 문자열 안에 변수 값을 섞어 넣거나, 여러 줄로 된 문자열을 만들 때는 코드가 복잡해질 수 있다.
ES6에서는 이런 불편함을 줄이기 위해 템플릿 리터럴을 사용할 수 있다.
템플릿 리터럴은 문자열을 작성하는 새로운 방식이다.
기존처럼 큰따옴표나 작은따옴표를 사용하는 대신 백틱 기호로 문자열을 감싼다.
템플릿 리터럴은 문자열 안에 변수나 연산 결과를 자연스럽게 넣고, 줄바꿈까지 그대로 표현할 수 있게 해 주는ES6문자열 작성 방식이다.
기존 문자열 연결 방식의 불편함
기존 방식에서는 문자열과 변수를 함께 출력하려면
+연산자를 사용해야 한다.
문자열이 길어질수록 따옴표와+가 많아지고, 공백을 빼먹기도 쉽다.// template-literal-old-string.js const js = "자바스크립트"; // 문자열 값을 변수에 저장 console.log("이건 " + js + "입니다."); // +로 문자열과 변수 연결// 출력결과 // 이건 자바스크립트입니다.결과는 맞지만 문자열을 여러 조각으로 나누어야 한다.
변수가 여러 개 들어가면 문장 구조가 한눈에 보이지 않는다.
템플릿 리터럴은 백틱과 ${}를 사용한다
템플릿 리터럴은 문자열을 백틱으로 감싼다.
그리고 문자열 안에 변수나 연산식을 넣고 싶으면${}를 사용한다.
${}안에는 실행하면 하나의 값으로 바뀌는 표현식을 넣을 수 있다.// template-literal-basic.js const js = "자바스크립트"; // 문자열 값을 변수에 저장 console.log(`이건 ${js}입니다.`); // ${} 안의 값을 문자열에 삽입// 출력결과 // 이건 자바스크립트입니다.
"${js}"가 아니라 백틱 안의${js}이기 때문에 변수 값이 문자열 안에 들어간다.
템플릿 리터럴의 핵심은 문자열을 조각내서 이어 붙이는 것이 아니라, 하나의 문장 안에 값을 끼워 넣는 방식으로 작성한다는 점이다.
${} 안에는 연산도 넣을 수 있다
${}안에는 변수만 넣을 수 있는 것이 아니다.
계산식도 넣을 수 있다.
계산식은 실행되면 하나의 값으로 바뀌기 때문에, 그 결과가 문자열 안에 들어간다.// template-literal-expression.js const num1 = 10; // 첫 번째 숫자 저장 const num2 = 20; // 두 번째 숫자 저장 console.log(`두 수의 합은 ${num1 + num2}입니다.`); // ${} 안에서 덧셈 실행// 출력결과 // 두 수의 합은 30입니다.
${num1 + num2}안에서는 먼저10 + 20계산이 실행된다.
그 결과인30이 문자열 안에 들어간다.
줄바꿈이 있는 문자열도 그대로 작성할 수 있다
기존 따옴표 문자열은 줄바꿈을 그대로 작성할 수 없다.
여러 줄 문자열을 표현하려면\n을 넣거나 문자열을 여러 번 이어 붙여야 한다.
템플릿 리터럴은 백틱 안에서 줄바꿈을 그대로 작성할 수 있다.// template-literal-multiline.js const message = `첫 번째 줄입니다. 두 번째 줄입니다. 세 번째 줄입니다.`; // 백틱 안의 줄바꿈이 문자열에 반영 console.log(message); // 여러 줄 문자열 출력// 출력결과 // 첫 번째 줄입니다. // 두 번째 줄입니다. // 세 번째 줄입니다.줄바꿈이 필요한 안내 문구, 여러 줄 메시지, 긴 문자열을 작성할 때 템플릿 리터럴은 훨씬 읽기 쉽다.
따옴표와 백틱은 결과가 다르다
${}는 아무 문자열에서나 동작하는 것이 아니다.
반드시 백틱으로 감싼 문자열 안에서 사용해야 한다.
큰따옴표나 작은따옴표 안에${}를 쓰면 변수 값이 들어가지 않고 글자 그대로 출력된다.// template-literal-backtick-warning.js const userName = "kim"; // 이름 값 저장 console.log("안녕하세요 ${userName}"); // 큰따옴표에서는 ${}가 동작하지 않음 console.log(`안녕하세요 ${userName}`); // 백틱에서는 ${}가 동작함// 출력결과 // 안녕하세요 ${userName} // 안녕하세요 kim
${}로 값을 넣고 싶다면 문자열을 큰따옴표나 작은따옴표가 아니라 백틱으로 감싸야 한다.
템플릿 리터럴 핵심 정리
- 템플릿 리터럴은 백틱으로 문자열을 감싼다.
- 변수나 연산 결과는
${}안에 넣는다.${}안에는 변수뿐 아니라 계산식도 넣을 수 있다.- 백틱 안에서는 줄바꿈이 그대로 문자열에 반영된다.
- 큰따옴표나 작은따옴표 안에서는
${}가 동작하지 않는다.템플릿 리터럴은 단순히 문자열을 짧게 쓰는 것만이 목적이 아니라, 문자열 안에 값이 들어가는 위치를 더 분명하게 보여 주기 위한 문법이다.
JavaScript에서 함수는 특정 동작을 묶어 두고, 필요할 때 다시 실행하기 위한 코드 묶음이다.
예를 들어 두 숫자를 더하는 코드가 여러 번 필요하다면, 그 코드를 함수로 만들어 두고 필요할 때마다 호출해서 사용할 수 있다.
기존에는 함수를 만들 때function키워드를 사용했다.
이 방식도 문제없이 사용할 수 있지만, 간단한 동작을 하는 함수까지 매번function, 중괄호,return을 모두 작성하면 코드가 길어질 수 있다.
ES6에서는 이런 함수를 더 짧고 간결하게 작성하기 위해 화살표 함수를 사용할 수 있다.
화살표 함수는 함수 표현식을 더 간단하게 작성하는 문법이다.
여기서 함수 표현식은 함수를 값처럼 만들어 변수에 저장하는 방식이라고 이해하면 된다.
화살표 함수는 기존 함수 표현식을 더 짧게 작성할 수 있게 해 주는ES6함수 문법이다.
이 구간에서는 먼저 같은 계산 결과를 만드는 함수를 더 간결하게 표현하는 방식으로 이해하면 된다.
다만 화살표 함수는this를 다루는 방식이 기존 함수와 다르므로, 완전히 같은 함수라고 생각하면 안 된다.
기존 함수 표현식은 function 키워드를 사용한다
기존 함수 표현식은
function키워드로 함수를 만들고, 그 함수를 변수에 저장한다.
함수를 변수에 저장해 두면, 나중에 그 변수 이름으로 함수를 호출할 수 있다.
아래 예제는 두 숫자를 더해서 결과를 반환하는 함수 표현식이다.// arrow-function-old-basic.js const sum = function(a, b) { // 두 값을 받는 함수 저장 return a + b; // 두 값을 더한 결과 반환 }; console.log(sum(3, 5)); // 함수 호출 후 결과 출력// 출력결과 // 8이 코드에서
sum은 함수를 저장한 변수이다.
function(a, b)에서a와b는 매개변수이다.
매개변수는 함수가 실행될 때 바깥에서 값을 받아 오는 자리이다.
sum(3, 5)처럼 함수를 호출하면a에는3,b에는5가 들어간다.
그리고return a + b가 실행되어8이 반환된다.
여기서return은 함수가 계산한 결과를 함수 밖으로 돌려주는 역할을 한다.
이 방식은 정확하지만 코드가 조금 길다.
특히 함수 안에서 하는 일이 한 줄뿐이라면, 매번function,{},return을 모두 쓰는 것이 답답하게 느껴질 수 있다.
이때 화살표 함수를 사용하면 같은 계산 흐름을 더 짧게 쓸 수 있다.
화살표 함수는 => 기호로 함수를 표현한다
화살표 함수는
function키워드 대신=>기호를 사용한다.
그래서 이름 그대로 화살표 함수라고 부른다.
기본 형태는 아래처럼 이해하면 된다.
- 왼쪽에는 함수가 받을 매개변수를 적는다.
- 가운데에는
=>를 적는다.- 오른쪽에는 함수가 실행할 코드를 적는다.
즉, 화살표 함수는 “이 값을 받아서 이런 결과를 만든다”는 흐름을 짧게 표현한다.
위에서 본sum함수를 화살표 함수로 바꾸면 아래처럼 된다.// arrow-function-basic.js const sum = (a, b) => a + b; // 두 값을 받아 더한 결과를 바로 반환 console.log(sum(3, 5)); // 함수 호출 후 결과 출력// 출력결과 // 8이 코드에서
(a, b)는 매개변수이다.
=>는 화살표 함수임을 나타내는 기호이다.
a + b는 함수가 실행할 코드이자 반환할 결과이다.
여기서는 중괄호와return이 없다.
이유는 중괄호 없이a + b라는 표현식 하나를 바로 작성했기 때문이다.
표현식은 실행하면 하나의 값으로 바뀌는 코드이다.
즉,(a, b) => a + b는 “a와b를 받아서a + b의 결과를 반환한다”는 뜻이다.
기존 함수 표현식과 비교하면 아래처럼 볼 수 있다.// arrow-function-compare.js const oldSum = function(a, b) { // 기존 함수 표현식 return a + b; // 결과 반환 }; const arrowSum = (a, b) => a + b; // 화살표 함수 표현식 console.log(oldSum(10, 20)); // 기존 함수 호출 console.log(arrowSum(10, 20)); // 화살표 함수 호출// 출력결과 // 30 // 30두 함수는 이 예제에서 같은 결과를 만든다.
하지만 화살표 함수는 더 짧다.
function키워드를 쓰지 않고, 중괄호 없이 표현식 하나를 바로 작성하면 그 결과가 자동으로 반환되기 때문이다.
화살표 함수는 간단한 함수를 한 줄로 표현할 때 코드의 의도를 더 빠르게 보여 준다.
단순한 매개변수 하나라면 괄호를 생략할 수 있다
화살표 함수에서 매개변수가 단순한 이름 하나뿐이라면 매개변수를 감싸는 괄호를 생략할 수 있다.
여기서 매개변수는 함수가 바깥에서 값을 받는 자리이다.
아래 예제는 숫자 하나를 받아 두 배로 만드는 함수이다.// arrow-function-one-parameter.js const double = number => number * 2; // 단순한 매개변수 하나라 괄호 생략 console.log(double(7)); // 함수 호출 후 결과 출력// 출력결과 // 14이 코드에서
number => number * 2는number하나를 받아서number * 2를 반환한다는 뜻이다.
매개변수가 단순한 이름 하나뿐이기 때문에(number)처럼 괄호로 감싸지 않아도 된다.
괄호를 써도 결과는 같다.// arrow-function-one-parameter-with-parentheses.js const double = (number) => number * 2; // 괄호를 써도 같은 의미 console.log(double(7)); // 함수 호출 후 결과 출력// 출력결과 // 14즉, 단순한 매개변수 하나일 때 괄호 생략은 선택이다.
코드를 더 짧게 쓰고 싶으면 생략할 수 있고, 통일감을 위해 괄호를 남길 수도 있다.
하지만 매개변수가 여러 개라면 괄호를 생략할 수 없다.
여러 값을 받는 함수는(a, b)처럼 괄호 안에 매개변수를 나열해야 한다.
이 단계에서는 “매개변수 이름 하나만 단순히 받는 경우에는 괄호를 생략할 수 있다” 정도로 이해하면 충분하다.
나중에 기본값이나 구조분해할당처럼 더 복잡한 매개변수 형태가 나오면 괄호가 필요할 수 있다.
매개변수가 없으면 괄호를 반드시 써야 한다
화살표 함수에서 매개변수가 하나도 없을 때는 빈 괄호를 써야 한다.
받을 값이 없더라도, 함수가 매개변수를 받지 않는다는 자리를 표시해야 하기 때문이다.
아래 예제는 매개변수 없이 정해진 문자열을 반환하는 함수이다.// arrow-function-no-parameter.js const sayHello = () => "안녕하세요"; // 매개변수가 없어서 빈 괄호 사용 console.log(sayHello()); // 함수 호출 후 결과 출력// 출력결과 // 안녕하세요이 코드에서
()는 받을 매개변수가 없다는 뜻이다.
그 뒤의=> "안녕하세요"는 이 함수가"안녕하세요"라는 문자열을 반환한다는 뜻이다.
매개변수가 없는데 괄호를 생략하면 올바른 화살표 함수 형태가 되지 않는다.
그래서 매개변수가 없을 때는 반드시()를 써야 한다.
정리하면 매개변수 괄호 규칙은 아래처럼 볼 수 있다.
- 매개변수가 여러 개면 괄호가 필요하다.
- 단순한 매개변수 하나면 괄호를 생략할 수 있다.
- 매개변수가 없으면 빈 괄호가 필요하다.
이 규칙을 알면 화살표 함수 모양이 달라도 같은 문법 흐름으로 읽을 수 있다.
중괄호를 생략하면 표현식 결과가 자동 반환된다
화살표 함수에서 중괄호 없이 표현식 하나를 바로 작성하면, 그 표현식의 결과가 자동으로 반환된다.
여기서 중요한 기준은 단순히 줄 수가 아니라 중괄호를 사용했는지, 표현식 하나를 바로 작성했는지이다.
아래 예제는 숫자 하나를 받아1을 더한 값을 반환한다.// arrow-function-implicit-return.js const addOne = number => number + 1; // 표현식 결과를 자동 반환 console.log(addOne(9)); // 함수 호출 후 결과 출력// 출력결과 // 10이 코드에는
return이 없다.
하지만number + 1의 결과가 자동으로 반환된다.
이런 방식은 함수가 간단한 계산 하나만 처리할 때 유용하다.
하지만 중괄호를 사용하면 이야기가 달라진다.
중괄호는 함수 몸체를 블록으로 작성한다는 뜻이다.
블록 안에서는 여러 줄의 코드를 작성할 수 있지만, 결과를 반환하려면return을 직접 써야 한다.// arrow-function-explicit-return.js const addOne = number => { // 중괄호를 사용한 함수 몸체 return number + 1; // return으로 결과를 직접 반환 }; console.log(addOne(9)); // 함수 호출 후 결과 출력// 출력결과 // 10이 예제는 중괄호 안에서
return number + 1을 직접 작성했다.
그래서 결과가 정상적으로10으로 반환된다.
반대로 중괄호를 사용했는데return을 쓰지 않으면 함수는 계산 결과를 밖으로 돌려주지 않는다.
아래 코드는 잘못 이해하기 쉬운 예시이다.// arrow-function-missing-return.js const addOne = number => { // 중괄호를 사용한 함수 몸체 number + 1; // 계산만 하고 반환하지 않음 }; console.log(addOne(9)); // 반환값 확인// 출력결과 // undefined이 코드에서
number + 1은 계산되지만, 그 결과가return으로 반환되지 않는다.
그래서addOne(9)의 결과는undefined가 된다.
여기서undefined는 함수가 돌려준 값이 없다는 의미로 이해하면 된다.
화살표 함수에서 중괄호 없이 표현식 하나를 쓰면 결과가 자동 반환되고, 중괄호를 사용하면return을 직접 써야 한다.
이 차이를 모르면 함수 안에서 계산은 했는데 결과가 나오지 않는 상황을 만나게 된다.
여러 줄 코드가 필요하면 중괄호를 사용한다
함수 안에서 해야 할 일이 여러 줄이라면 중괄호를 사용한다.
예를 들어 중간 값을 변수에 저장하고, 그 값을 다시 가공해서 반환해야 한다면 한 줄로 끝내기 어렵다.
아래 예제는 가격에 배송비를 더한 뒤, 최종 금액 안내 문장을 반환한다.// arrow-function-multiple-lines.js const makeTotalMessage = (price, deliveryFee) => { // 가격과 배송비를 받는 함수 const total = price + deliveryFee; // 최종 금액 계산 return `최종 금액은 ${total}원입니다.`; // 안내 문장 반환 }; console.log(makeTotalMessage(10000, 3000)); // 함수 호출 후 결과 출력// 출력결과 // 최종 금액은 13000원입니다.이 함수는 한 줄 계산만 하는 함수가 아니다.
먼저total이라는 변수를 만들어 최종 금액을 계산한다.
그다음 템플릿 리터럴을 사용해서 안내 문장을 만든 뒤 반환한다.
이처럼 함수 안에서 여러 줄의 처리가 필요하면 중괄호를 사용한다.
그리고 중괄호를 사용했기 때문에 마지막 결과를 밖으로 보내려면return을 직접 써야 한다.
화살표 함수는 this를 직접 만들어 가지지 않는다
화살표 함수는 기존 함수 표현식을 짧게 쓰기 위한 문법이지만, 기존 함수와 완전히 같은 것은 아니다.
가장 중요한 차이 중 하나는this를 다루는 방식이다.
this는 함수가 실행될 때 “지금 이 함수가 어떤 대상과 연결되어 실행되는가”를 가리키는 값이다.
객체 메서드에서는 보통 그 메서드를 호출한 객체를 가리키는 데 사용한다.
일반 메서드에서는this가 메서드를 호출한 객체를 가리킬 수 있다.// arrow-function-this-normal-method.js const user = { nickname: "짱구", // 객체 프로퍼티 sayName() { // 일반 메서드 return this.nickname; // this는 user 객체를 가리킴 } }; console.log(user.sayName()); // 메서드 호출 결과 출력// 출력결과 // 짱구이 코드에서
user.sayName()으로 메서드를 호출했다.
그래서sayName()안의this는user객체를 가리킨다.
그 결과this.nickname은"짱구"가 된다.
반면 화살표 함수는 자기만의this를 새로 만들지 않는다.
그래서 객체의 메서드로 작성했을 때 “이 객체 자신”을 가리키는 용도로 쓰면 예상과 다르게 동작할 수 있다.
초보자 단계에서는 이렇게 기준을 잡으면 된다.
- 단순 계산 함수나 배열 메서드 안의 짧은
callback함수에는 화살표 함수를 자주 사용한다.- 객체가 자기 자신의 프로퍼티를
this로 읽어야 하는 메서드라면 일반 메서드 문법을 사용하는 것이 안전하다.화살표 함수는 짧게 쓰기 편하지만, 자기만의
this를 만들지 않으므로 객체 메서드로 사용할 때는 주의해야 한다.
이 구간에서는 화살표 함수를 “간단한 함수 표현식과callback을 짧게 쓰는 문법”으로 먼저 이해하면 된다.
화살표 함수 핵심 정리
화살표 함수는
ES6에서 추가된 함수 작성 방식이다.
기존 함수 표현식보다 짧게 작성할 수 있고, 특히 간단한 계산이나 값을 반환하는 함수에서 코드가 더 읽기 쉬워진다.
핵심은 아래처럼 정리할 수 있다.
- 화살표 함수는
function키워드 대신=>를 사용한다.- 매개변수가 여러 개면 괄호를 사용한다.
- 단순한 매개변수 하나면 괄호를 생략할 수 있다.
- 매개변수가 없으면 빈 괄호를 반드시 사용한다.
- 중괄호 없이 표현식 하나를 작성하면 그 결과가 자동 반환된다.
- 중괄호를 사용하면 결과 반환을 위해
return을 직접 써야 한다.- 화살표 함수는 자기만의
this를 만들지 않는다.- 객체가 자기 자신의 값을
this로 읽어야 하는 메서드라면 일반 메서드 문법이 더 안전하다.화살표 함수는 함수를 무조건 짧게 쓰기 위한 문법이 아니라, 간단한 함수의 입력과 결과를 더 분명하게 보여 주기 위한 문법이다.
특히 배열 메서드처럼 함수를 다른 함수의 인자로 전달하는 코드에서 자주 사용되므로, 기본 형태를 정확히 읽을 수 있어야 한다.
JavaScript코드가 짧을 때는 하나의 파일에 모든 코드를 작성해도 크게 불편하지 않다.
하지만 코드가 길어지면 변수, 함수, 클래스가 한 파일 안에 계속 쌓인다.
그러면 어떤 코드가 어떤 역할을 하는지 찾기 어려워지고, 수정할 때도 다른 코드에 영향을 줄 수 있다.
이 문제를 줄이기 위해 사용하는 개념이 모듈이다.
모듈은 영어로module이고, 코드를 역할별로 나누어 관리하는 방식이다.
다만 파일을 여러 개로 나누었다고 해서 모든 파일이 자동으로 모듈이 되는 것은 아니다.
브라우저 기준에서는script type="module"로 실행되는 파일이 모듈의 시작점이 되고, 그 파일에서import로 연결되는 파일들도 모듈 흐름으로 함께 동작한다.
모듈은 단순히 파일을 여러 개로 나누는 것이 아니라,export와import를 통해 필요한 값만 공개하고 가져오도록 연결하는 구조이다.
모듈은 왜 필요한가
모듈은 코드를 역할별로 나누기 위해 필요하다.
사용자 정보와 관련된 코드는 사용자 정보 파일에 두고, 계산과 관련된 코드는 계산 파일에 두고, 실제 실행 흐름은 실행 파일에 둘 수 있다.
이렇게 나누면 한 파일이 담당하는 일이 분명해진다.
모듈이 필요한 이유는 아래처럼 정리할 수 있다.
- 코드가 길어졌을 때 역할별로 나눌 수 있다.
- 필요한 코드만 다른 파일에서 가져다 쓸 수 있다.
- 파일마다 맡은 역할이 분명해져서 유지보수가 쉬워진다.
- 같은 기능을 여러 곳에서 다시 사용할 수 있다.
- 어떤 값이 외부에 공개되는지 분명하게 관리할 수 있다.
즉, 모듈은 코드를 멋있게 나누기 위한 문법이 아니라 코드가 커졌을 때 관리하기 쉽게 만들기 위한 구조이다.
모듈은 독립적인 scope를 가진다
모듈은 자신만의 독립적인
scope를 가진다.
즉, 모듈 파일 안에서 만든 변수나 함수는 기본적으로 그 파일 안에서만 사용할 수 있다.
다른 파일에서 자동으로 보이지 않는다.
다른 파일에서 사용하려면 해당 값을export로 공개해야 하고, 사용하는 파일에서import로 가져와야 한다.
이 구조가 중요한 이유는 이름 충돌을 줄여 주기 때문이다.
모듈 내부에서 선언한 변수와 함수는 기본적으로 비공개이고, 다른 파일에서 사용하려면 반드시export와import로 연결해야 한다.
Named export와 import
export는 모듈 안에 있는 변수, 함수, 클래스 등을 다른 파일에서 사용할 수 있도록 공개하는 문법이다.
Named export는 이름이 있는 값을 그대로 내보내는 방식이다.
여러 개를 내보낼 수 있고, 가져올 때도 내보낸 이름과 같은 이름으로 가져와야 한다.// profile.js export const userName = "kim"; // userName 변수를 외부에 공개 export const userAge = 20; // userAge 변수를 외부에 공개 export function introduce() { // introduce 함수를 외부에 공개 return `${userName}은 ${userAge}살입니다.`; // 사용자 소개 문장 반환 }// app.js import { userName, userAge, introduce } from "./profile.js"; // 공개된 값 가져오기 console.log(userName); // 가져온 이름 출력 console.log(userAge); // 가져온 나이 출력 console.log(introduce()); // 가져온 함수 실행// 출력결과 // kim // 20 // kim은 20살입니다.
Named export로 내보낸 값은import할 때 이름을 맞춰야 하고, 반드시 중괄호 안에 적어야 한다.
Default export
Default export는 한 파일에서 대표로 내보낼 값 하나를 정하는 방식이다.
default는 기본값이라는 뜻으로, 이 파일을 대표하는 기본 내보내기라고 이해하면 된다.
Default export는 한 파일에서 하나만 사용할 수 있다.// calculator.js export default function add(a, b) { // 이 파일의 대표 함수로 공개 return a + b; // 두 값을 더한 결과 반환 }// calculator-app.js import addNumber from "./calculator.js"; // default export는 원하는 이름으로 가져올 수 있음 console.log(addNumber(10, 20)); // 가져온 함수 실행// 출력결과 // 30
Default export는 가져올 때 중괄호를 사용하지 않는다.
그리고 가져오는 쪽에서 이름을 자유롭게 정할 수 있다.
이름을 맞춰 중괄호로 가져오면Named export, 중괄호 없이 자유로운 이름으로 가져오면Default export라고 구분하면 된다.
as와 * as
as는 가져온 이름을 현재 파일에서 다른 이름으로 사용할 때 쓴다.
* as는 해당 모듈에서 공개한 모든 값을 하나의 객체처럼 묶어 가져오는 방식이다.// profile-alias-app.js import { userName as name, userAge as age } from "./profile.js"; // 가져온 이름을 바꿔 사용 console.log(name); // 바꾼 이름으로 출력 console.log(age); // 바꾼 이름으로 출력// 출력결과 // kim // 20// profile-all-app.js import * as Profile from "./profile.js"; // 공개 값을 Profile로 묶어 가져오기 console.log(Profile.userName); // 묶음 안의 userName 사용 console.log(Profile.userAge); // 묶음 안의 userAge 사용 console.log(Profile.introduce()); // 묶음 안의 introduce 함수 실행// 출력결과 // kim // 20 // kim은 20살입니다.
브라우저에서 모듈을 사용하려면 type="module"이 필요하다
브라우저에서
import와export를 직접 사용하려면script태그에type="module"을 적어야 한다.
이 속성은 “이JavaScript파일을 일반 스크립트가 아니라 모듈로 실행하겠다”는 뜻이다.// module-index.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>module example</title> </head> <body> <h1>module example</h1> <script type="module" src="./app.js"></script> <!-- app.js를 모듈 시작 파일로 실행 --> </body> </html>브라우저에서
import와export를 사용하는 파일은<script type="module">로 연결해야 하고, 그 파일이 모듈의 시작점이 된다.
모듈은 자동으로 엄격 모드와 지연 실행 특징을 가진다
모듈은 단순히 파일을 나누는 기능만 가진 것이 아니다.
브라우저에서 모듈로 실행되는JavaScript파일은 일반script와 다른 특징을 가진다.
첫 번째 특징은Strict Mode이다.
Strict Mode는 실수하기 쉬운 문법 사용을 더 엄격하게 검사하는 실행 방식이다.
일반script에서는"use strict"를 직접 적어야 엄격 모드가 적용될 수 있지만, 모듈에서는 따로 적지 않아도 자동으로 엄격 모드가 적용된다.
그래서 모듈 안에서는 더 안전한 기준으로 코드가 실행된다.
두 번째 특징은Deferred Evaluation이다.
Deferred Evaluation은 실행이 뒤로 미뤄지는 특징이라고 이해하면 된다.
브라우저에서<script type="module">로 연결된 모듈 스크립트는HTML문서가 파싱된 뒤 실행된다.
즉, 일반script처럼 문서 읽는 흐름을 바로 막기보다, 기본적으로defer속성을 가진 것처럼 동작한다.
세 번째 특징은Singleton이다.
Singleton은 같은 대상이 여러 번 만들어지지 않고 하나만 유지되는 구조를 말한다.
모듈에서는 같은 파일을 여러 곳에서import하더라도 그 모듈 파일이 매번 새로 실행되는 것이 아니다.
처음 한 번 실행된 결과가 캐싱되고, 이후에는 그 결과를 다시 사용한다.
여기서 캐싱은 한 번 만들어 둔 결과를 저장해 두었다가 다시 사용하는 것을 의미한다.
정리하면 모듈의 주요 특징은 아래와 같다.
- 모듈은 자동으로
Strict Mode가 적용된다.<script type="module">은 기본적으로defer처럼 동작한다.- 같은 모듈을 여러 번
import해도 모듈은 한 번만 실행되고 결과가 재사용된다.모듈은 필요한 값을 공개하고 가져오는 구조일 뿐 아니라, 독립적인
scope, 자동 엄격 모드, 지연 실행, 캐싱 특징까지 함께 가진 실행 단위이다.
모듈 핵심 정리
- 모듈은 코드를 역할별 파일로 나누어 연결하는 구조이다.
- 브라우저에서는
type="module"로 실행되는 파일이 모듈의 시작점이다.- 모듈은 자신만의 독립적인
scope를 가진다.- 모듈 내부 코드는 자동으로
Strict Mode로 실행된다.- 모듈 스크립트는 기본적으로 문서 파싱 후 실행되는
defer흐름을 가진다.- 같은 모듈을 여러 번
import해도 한 번만 실행되고 결과가 재사용된다.- 다른 파일에서 사용할 값은
export로 공개한다.- 공개된 값은
import로 가져온다.Named export는 이름을 맞춰 중괄호로 가져온다.Default export는 중괄호 없이 원하는 이름으로 가져온다.as는 가져온 이름을 현재 파일에서 바꿔 쓸 때 사용한다.* as는 공개된 값을 하나의 묶음처럼 가져올 때 사용한다.모듈은 코드를 파일로 나누는 문법이 아니라, 필요한 값만 명확하게 공개하고 가져오면서 코드의 역할과 연결 관계를 정리하는 구조이다.
JavaScript에서 객체는 여러 값을 하나로 묶어 다루기 위한 데이터이다.
클래스는 이런 객체를 만들기 위한 틀이다.
여기서 틀이라는 말은 같은 구조의 객체를 계속 만들 수 있게 해 주는 설계도라고 이해하면 된다.
ES6에서는class문법을 사용해서 객체를 만드는 구조를 더 읽기 쉽게 표현할 수 있다.
기존에도 함수와 프로토타입을 사용해서 객체 생성 구조를 만들 수 있었지만, 초보자가 읽기에는 흐름이 복잡할 수 있다.
class는 객체를 만들기 위한 구조를 더 명확한 형태로 보여 준다.
클래스는 객체를 직접 의미하는 것이 아니라, 객체를 만들기 위한 설계도이다.
클래스를 작성한 뒤new를 사용하면 그 설계도를 바탕으로 실제 객체를 만들 수 있다.
이렇게 클래스로 만든 실제 객체를 인스턴스라고 한다.
클래스는 정의한 뒤 new로 객체를 만든다
클래스는 객체를 만들기 위한 설계도이지만, 클래스 자체가 곧 객체는 아니다.
클래스를 사용하려면 먼저 클래스를 정의하고, 그다음new를 사용해서 실제 객체를 만들어야 한다.
클래스는 함수처럼 보이는 부분이 있지만 일반 함수처럼 직접 호출할 수 없다.
예를 들어Person()처럼 그냥 호출하는 것이 아니라,new Person()처럼 객체 생성 문법으로 호출해야 한다.
그렇지 않으면 클래스 생성 규칙에 맞지 않아 오류가 난다.
또 클래스 선언은let,const처럼 블록scope를 가진다.
그래서 클래스가 선언된 블록 안에서만 사용할 수 있다.
그리고 클래스를 사용하려면 클래스 선언이 먼저 코드에 나와 있어야 한다.
초보자 단계에서는 “클래스는 먼저 만들고, 그 아래에서new로 객체를 생성한다”라고 이해하면 안전하다.
아래 코드는 클래스를 정의한 뒤 객체를 만드는 기본 흐름이다.// class-create-after-define.js class Person { // Person 클래스 정의 constructor(name) { // 객체 생성 시 이름을 받음 this.name = name; // 객체에 name 저장 } } const person = new Person("kim"); // new로 실제 객체 생성 console.log(person.name); // 생성된 객체의 값 출력// 출력결과 // kim이 코드에서
Person은 객체를 만들기 위한 클래스이고,person은new Person("kim")으로 실제 만들어진 객체이다.
즉, 클래스와 인스턴스는 같은 것이 아니다.
클래스는 설계도이고, 인스턴스는 그 설계도로 만들어진 실제 객체이다.
아래 코드는 잘못된 예시이다.// class-call-without-new-error.js class Person { // Person 클래스 정의 constructor(name) { // 객체 생성 시 이름을 받음 this.name = name; // 객체에 name 저장 } } const person = Person("kim"); // new 없이 클래스 호출 시도 console.log(person); // 실행되지 않음// 출력결과 // TypeError: Class constructor Person cannot be invoked without 'new'클래스는 일반 함수처럼 직접 호출할 수 없다.
Person("kim")처럼 호출하면 객체 생성 흐름이 아니므로 오류가 발생한다.
클래스는 먼저 정의한 뒤new로 인스턴스를 만들어 사용해야 하며, 일반 함수처럼 직접 호출하면 안 된다.
constructor와 this
constructor는 객체가 만들어질 때 처음 실행되는 특별한 메서드이다.
새 객체에 처음 필요한 값을 저장할 때 사용한다.
this는 지금 만들어지는 객체 자신을 가리킨다고 이해하면 된다.
아래 예제는Person클래스로 사람 객체를 만드는 코드이다.// class-person-basic.js class Person { constructor(name, age) { // 객체 생성 시 이름과 나이를 받음 this.name = name; // 객체의 name 값 저장 this.age = age; // 객체의 age 값 저장 } nextYearAge() { // 내년 나이를 계산하는 메서드 return Number(this.age) + 1; // 나이를 숫자로 바꾼 뒤 1 더하기 } } const person = new Person("kim", "23"); // Person 클래스로 객체 생성 console.log(person.name); // 객체의 이름 출력 console.log(person.age); // 객체의 나이 출력 console.log(person.nextYearAge()); // 메서드 호출 결과 출력// 출력결과 // kim // 23 // 24
new Person("kim", "23")을 실행하면Person클래스를 바탕으로 새 객체가 만들어진다.
그 순간constructor(name, age)가 실행되고, 전달한 값이this.name,this.age에 저장된다.
nextYearAge()는 클래스 안에 작성한 메서드이다.
메서드는 객체가 가지고 있는 함수라고 이해하면 된다.
person.nextYearAge()처럼 객체를 통해 호출하면,this.age에 저장된 값을 사용해서 내년 나이를 계산한다.
constructor는 객체가 만들어지는 순간 초기값을 받아 객체 안에 저장하는 역할을 한다.
extends와 super
상속은 기존 클래스가 가진 기능을 다른 클래스가 이어받는 것이다.
extends는 다른 클래스를 상속해서 기능을 확장한다는 뜻이다.
super()는 부모 클래스의constructor를 호출한다.
여기서 부모 클래스는 기능을 물려주는 클래스이고, 자식 클래스는 그 기능을 물려받아 확장하는 클래스이다.
자식 클래스에서 부모의 초기값 저장 과정이 필요하면super()를 먼저 호출해야 한다.// class-extends-basic.js class Person { constructor(name, age) { // 부모 클래스 생성자 this.name = name; // 이름 저장 this.age = age; // 나이 저장 } nextYearAge() { // 부모 클래스 메서드 return Number(this.age) + 1; // 내년 나이 반환 } } class IntroducePerson extends Person { constructor(name, age, city, futureHope) { // 자식 클래스 생성자 super(name, age); // 부모 생성자 호출 this.city = city; // 도시 저장 this.futureHope = futureHope; // 장래희망 저장 } introduce() { // 소개 문장을 만드는 메서드 return `저는 ${this.city}에 사는 ${this.name}입니다. 내년엔 ${super.nextYearAge()}살이며, 장래희망은 ${this.futureHope}입니다.`; // 부모 메서드 결과 포함 } } const kim = new IntroducePerson("kim", "23", "seoul", "개발자"); // 자식 클래스로 객체 생성 console.log(kim.introduce()); // 소개 문장 출력// 출력결과 // 저는 seoul에 사는 kim입니다. // 내년엔 24살이며, // 장래희망은 개발자입니다.
IntroducePerson은Person을 상속한다.
그래서Person이 가지고 있던name,age,nextYearAge()흐름을 사용할 수 있다.
자식 클래스의constructor안에서super(name, age)를 호출하면 부모 클래스의constructor가 먼저 실행된다.
그 결과this.name,this.age가 부모 클래스 방식으로 저장된다.
그 뒤 자식 클래스에서 필요한city,futureHope를 추가로 저장한다.
extends는 부모 클래스의 기능을 이어받기 위한 문법이고,super()는 부모 클래스의 생성자를 호출해서 부모 쪽 초기화를 먼저 처리하는 문법이다.
static 메서드는 객체를 만들지 않고 클래스 이름으로 호출한다
static이 붙은 메서드는 인스턴스를 만들지 않고 클래스 이름으로 바로 호출할 수 있다.
인스턴스는 클래스를 바탕으로 실제로 만들어진 객체이다.
일반 메서드는 객체가 가진 데이터를 사용해서 동작하는 경우가 많다.
반면static메서드는 특정 객체 하나의 데이터보다, 클래스와 관련된 공통 기능을 만들 때 사용할 수 있다.// class-static-method.js class Calculator { static add(a, b) { // static 메서드 선언 return a + b; // 두 값을 더한 결과 반환 } } console.log(Calculator.add(10, 20)); // 클래스 이름으로 static 메서드 호출// 출력결과 // 30이 예제에서는
new Calculator()로 객체를 만들지 않았다.
대신Calculator.add(10, 20)처럼 클래스 이름으로 바로 메서드를 호출했다.
static메서드는 객체마다 따로 필요한 동작이 아니라, 클래스 이름으로 직접 호출할 공통 기능을 만들 때 사용한다.
getter와 setter는 값을 읽고 바꾸는 통로를 만든다
getter는 객체의 값을 읽을 때 사용하는 메서드이다.
setter는 객체의 값을 바꿀 때 사용하는 메서드이다.
겉으로는 프로퍼티를 읽고 쓰는 것처럼 보이지만, 실제로는 클래스 안의 메서드가 실행된다.
이 구조를 사용하면 값을 그냥 바꾸는 것이 아니라, 바꾸기 전에 조건을 확인하거나 필요한 형태로 가공할 수 있다.
아래 예제는 이름을 읽을 때getter를 사용하고, 이름을 바꿀 때setter를 사용하는 코드이다.// class-getter-setter.js class Person { constructor(name) { // 객체 생성 시 이름을 받음 this._name = name; // 내부에서 사용할 이름 저장 } get name() { // name 값을 읽을 때 실행 return this._name; // 저장된 이름 반환 } set name(newName) { // name 값을 바꿀 때 실행 if (newName.length === 0) { // 빈 문자열인지 확인 return; // 빈 문자열이면 변경하지 않음 } this._name = newName; // 새 이름 저장 } } const person = new Person("kim"); // 객체 생성 console.log(person.name); // getter 실행 person.name = "lee"; // setter 실행 console.log(person.name); // 변경된 이름 확인// 출력결과 // kim // lee
console.log(person.name)처럼 값을 읽으면get name()이 실행된다.
person.name = "lee"처럼 값을 대입하면set name(newName)이 실행된다.
이 예제에서는 새 이름이 빈 문자열이면 값을 바꾸지 않도록 조건을 넣었다.
이처럼setter를 사용하면 값을 변경하기 전에 검사 로직을 넣을 수 있다.
여기서_name은 이름 앞에_를 붙인 일반 프로퍼티이다.
개발자가 내부에서 쓰겠다는 표시로 붙인 이름일 뿐, 클래스 밖에서 접근을 막는 진짜 비공개 문법은 아니다.
실제로 클래스 밖에서 접근을 막고 싶다면 다음 구간처럼#name형태의 비공개 프로퍼티를 사용해야 한다.
getter와setter는 객체의 값을 직접 읽고 바꾸는 것처럼 보이게 하면서, 실제로는 클래스 안의 메서드를 통해 값을 관리하게 해 준다.
비공개 프로퍼티는 클래스 안에서만 사용할 수 있다
비공개 프로퍼티는
#name처럼#을 붙여 선언한다.
이 문법은ES6의 기본class문법 자체에 포함된 문법이라기보다, 이후 현대JavaScript에서 클래스의 값을 더 안전하게 관리하기 위해 추가된 문법이다.
여기서는 클래스에서 값을 어디까지 공개하고 어디부터 숨길지 이해하기 위해 함께 정리한다.
비공개 프로퍼티는 클래스 밖에서 직접 접근할 수 없고, 클래스 안에서만 사용할 수 있다.
이 기능이 필요한 이유는 객체 안의 중요한 값을 아무 곳에서나 직접 바꾸지 못하게 하기 위해서이다.
값을 외부에 보여 주거나 바꿔야 한다면, 메서드를 통해 필요한 만큼만 공개할 수 있다.// class-private-property.js class Person { #name; // 비공개 프로퍼티 선언 constructor(name) { // 객체 생성 시 이름을 받음 this.#name = name; // 클래스 내부에서 비공개 값 저장 } getName() { // 비공개 값을 밖으로 보여 주는 메서드 return this.#name; // 클래스 내부에서는 접근 가능 } } const person = new Person("kim"); // 객체 생성 console.log(person.getName()); // 메서드를 통해 값 확인// 출력결과 // kim
#name은 클래스 안에서만 사용할 수 있다.
그래서 외부에서는person.#name처럼 직접 접근할 수 없다.
대신getName()처럼 공개된 메서드를 통해 필요한 값만 확인할 수 있다.
공개 프로퍼티는 밖에서 직접 읽고 바꿀 수 있고, 비공개 프로퍼티는 클래스 안에서만 다룰 수 있다.
이 차이를 알면 클래스에서 값을 어디까지 공개하고 어디부터 숨길지 판단할 수 있다.
클래스 핵심 정리
class는 객체를 만들기 위한 설계도이다.- 클래스는 일반 함수처럼 직접 호출할 수 없다.
- 클래스는 먼저 정의한 뒤 사용해야 한다.
new를 사용하면 클래스로 실제 객체를 만들 수 있다.- 클래스로 만든 실제 객체를 인스턴스라고 한다.
constructor는 객체가 만들어질 때 처음 실행되어 초기값을 저장한다.this는 지금 만들어진 객체 자신을 가리킨다.extends는 부모 클래스를 상속할 때 사용한다.super()는 부모 클래스의constructor를 호출할 때 사용한다.static메서드는 객체를 만들지 않고 클래스 이름으로 호출한다.getter는 값을 읽을 때 실행되는 메서드이다.setter는 값을 바꿀 때 실행되는 메서드이다._name처럼_를 붙이는 방식은 내부용 표시일 뿐, 진짜 비공개 문법은 아니다.- 비공개 프로퍼티는
#을 붙이고 클래스 안에서만 사용할 수 있다.클래스는 객체를 반복해서 만들기 위한 틀이면서, 상속과 접근 범위를 통해 객체 구조를 더 체계적으로 관리하게 해 주는 문법이다.
Rest Operator와Spread Operator는 둘 다...기호를 사용한다.
그래서 처음 보면 같은 문법처럼 보일 수 있다.
하지만 둘은 역할이 다르다.
Rest Operator는 여러 값을 하나로 묶는다.
반대로Spread Operator는 하나로 묶여 있는 값을 여러 개로 펼친다.
배열과 함수 매개변수에서 사용하는Rest/Spread흐름은ES6에서 자주 다루는 문법이고, 객체에서 사용하는Rest/Spread흐름은 이후 표준에서 추가된 현대JavaScript문법이다.
여기서는 실제 코드에서 함께 자주 쓰이므로 묶어서 정리한다.
Rest Operator는 값을 모아서 배열이나 객체로 묶는 역할이고,Spread Operator는 배열, 문자열, 객체처럼 묶인 값을 개별 값으로 펼치는 역할이다.
Rest Operator는 나머지 값을 하나로 묶는다
Rest Operator는 나머지 값들을 하나로 묶어 받는 문법이다.// rest-array-basic.js const numbers = [1, 2, 3, 4, 5, 6]; // 여러 숫자가 담긴 배열 const [first, second, ...others] = numbers; // 앞의 두 값은 따로 받고, 나머지는 배열로 묶음 console.log(first); // 첫 번째 값 출력 console.log(second); // 두 번째 값 출력 console.log(others); // 나머지 값 배열 출력// 출력결과 // 1 // 2 // [3, 4, 5, 6]배열에서
Rest Operator는 남은 여러 값을 하나의 배열로 모아 받는다.
함수 매개변수에서도 같은 흐름으로 사용할 수 있다.// rest-function-args.js function showNumbers(first, second, ...others) { // 앞의 두 값은 따로 받고 나머지는 배열로 묶음 console.log(first); // 첫 번째 인자 출력 console.log(second); // 두 번째 인자 출력 console.log(others); // 나머지 인자 배열 출력 } showNumbers(1, 2, 3, 4, 5); // 여러 값을 함수에 전달// 출력결과 // 1 // 2 // [3, 4, 5]
객체에서도 Rest Operator를 사용할 수 있다
객체에서는 먼저 꺼낸 프로퍼티를 제외하고 남은 프로퍼티들이 새 객체로 묶인다.
// rest-object-basic.js const user = { nickname: "짱구", // 닉네임 프로퍼티 species: "human", // 종족 프로퍼티 age: 5, // 나이 프로퍼티 city: "떡잎마을" // 도시 프로퍼티 }; const { nickname, ...profile } = user; // nickname은 따로 받고, 나머지는 객체로 묶음 console.log(nickname); // 따로 꺼낸 nickname 출력 console.log(profile); // 나머지 프로퍼티가 묶인 객체 출력// 출력결과 // 짱구 // { species: 'human', age: 5, city: '떡잎마을' }배열의
Rest Operator는 남은 요소를 배열로 묶고, 객체의Rest Operator는 남은 프로퍼티를 객체로 묶는다.
Rest Operator는 마지막에만 올 수 있다
Rest Operator는 나머지 값을 모으는 문법이다.
그래서 값을 받는 자리에서 마지막에 와야 한다.// rest-operator-position-error.js function wrongExample(...wrong, second) { // Rest Operator 뒤에 다른 매개변수가 와서 오류 console.log(wrong); // 실행되지 않음 console.log(second); // 실행되지 않음 } wrongExample(1, 2, 3); // 함수 호출 시도// 출력결과 // SyntaxError: Rest parameter must be last formal parameter
Rest Operator는 나머지를 모으는 문법이기 때문에 배열 구조분해할당, 객체 구조분해할당, 함수 매개변수에서 모두 마지막 위치에 작성해야 한다.
Spread Operator는 묶인 값을 개별 값으로 펼친다
Spread Operator는 배열, 문자열, 객체처럼 묶여 있는 값을 개별 값으로 펼치는 문법이다.// spread-array-basic.js const numbers = [1, 2, 3, 4, 5]; // 여러 숫자가 담긴 배열 console.log(numbers); // 배열 그대로 출력 console.log(...numbers); // 배열 안의 값을 개별 값으로 펼쳐 출력// 출력결과 // [1, 2, 3, 4, 5] // 1 2 3 4 5문자열도 여러 글자가 순서대로 모여 있는 값이므로
Spread Operator로 펼칠 수 있다.// spread-string-basic.js const language = "javascript"; // 문자열 저장 console.log(...language); // 문자열을 문자 단위로 펼쳐 출력// 출력결과 // j a v a s c r i p t
배열과 객체에서 Spread Operator 활용하기
Spread Operator는 배열을 복사하거나 합칠 때 사용할 수 있다.// spread-array-merge.js const firstNumbers = [1, 2]; // 첫 번째 배열 const secondNumbers = [3, 4]; // 두 번째 배열 const mergedNumbers = [...firstNumbers, ...secondNumbers]; // 두 배열의 값을 펼쳐 새 배열 생성 console.log(mergedNumbers); // 합쳐진 배열 출력// 출력결과 // [1, 2, 3, 4]객체에서도
Spread Operator를 사용할 수 있다.
객체에서...을 사용하면 객체의 프로퍼티를 펼쳐서 새 객체에 넣을 수 있다.// object-spread-add.js const user = { nickname: "짱구", species: "human" }; // 기존 객체 const newUser = { ...user, age: 5 }; // 기존 객체를 펼친 뒤 age를 추가한 새 객체 생성 console.log(user); // 기존 객체 출력 console.log(newUser); // 새 객체 출력// 출력결과 // { nickname: '짱구', species: 'human' } // { nickname: '짱구', species: 'human', age: 5 }
user.age = 5처럼 직접 추가하면 기존 객체가 수정된다.
반면{ ...user, age: 5 }는 기존 객체의 프로퍼티를 펼쳐 새 객체를 만든다.
객체Spread Operator의 핵심은 기존 객체를 직접 수정하는 것이 아니라, 기존 객체의 프로퍼티를 펼쳐 새 객체를 만드는 데 있다.
객체 Spread Operator는 얕은 복사를 만든다
객체
Spread Operator로 새 객체를 만들면 겉 객체는 새로 만들어진다.
하지만 객체 안에 또 다른 객체가 들어 있다면, 그 안쪽 객체까지 모두 새로 복사되는 것은 아니다.
이런 복사를 얕은 복사라고 한다.// object-spread-shallow-copy.js const user = { nickname: "짱구", // 바깥 객체의 프로퍼티 profile: { // 안쪽 객체 city: "떡잎마을" // 안쪽 객체의 프로퍼티 } }; const copiedUser = { ...user }; // 바깥 객체만 새로 복사 copiedUser.profile.city = "서울"; // 안쪽 객체의 값을 변경 console.log(user.profile.city); // 원본 객체의 안쪽 값 확인 console.log(copiedUser.profile.city); // 복사본 객체의 안쪽 값 확인 console.log(user === copiedUser); // 바깥 객체 비교 console.log(user.profile === copiedUser.profile); // 안쪽 객체 비교// 출력결과 // 서울 // 서울 // false // true
user === copiedUser는false이므로 바깥 객체는 새로 만들어졌다.
하지만user.profile === copiedUser.profile은true이므로 안쪽 객체는 같은 대상을 공유한다.
객체Spread Operator는 바깥 객체를 새로 만들지만, 중첩 객체까지 자동으로 깊게 복사하지는 않는다.
객체 전개에서는 뒤에 있는 값이 앞의 값을 덮어쓴다
객체에서
Spread Operator를 사용할 때 같은 프로퍼티 이름이 여러 번 나오면, 뒤에 작성한 값이 앞의 값을 덮어쓴다.// spread-object-overwrite.js let user = { nickname: "짱구", species: "human" }; // 기존 객체 user = { ...user, age: 5 }; // 기존 객체를 펼치고 age 추가 console.log(user); // age가 추가된 객체 출력 user = { ...user, nickname: "짱아", age: 1 }; // 기존 값 중 nickname과 age를 뒤에서 덮어쓰기 console.log(user); // 덮어쓴 결과 출력// 출력결과 // { nickname: '짱구', species: 'human', age: 5 } // { nickname: '짱아', species: 'human', age: 1 }객체에서
Spread Operator를 사용할 때는 같은 프로퍼티가 있으면 뒤에 작성한 값이 최종 값이 된다.
Rest와 Spread 핵심 정리
Rest Operator는 나머지 값을 하나로 묶는다.- 배열에서
Rest Operator는 남은 요소를 배열로 묶는다.- 객체에서
Rest Operator는 남은 프로퍼티를 객체로 묶는다.- 값을 받는 자리에서
Rest Operator는 마지막에 와야 한다.Spread Operator는 배열, 문자열, 객체처럼 묶인 값을 개별 값으로 펼친다.- 객체
Spread Operator를 사용하면 기존 객체의 값을 바탕으로 새 객체를 만들 수 있다.- 객체
Spread Operator는 얕은 복사이므로 중첩 객체는 공유될 수 있다.- 같은
...이라도 값을 받아 모으는 자리이면Rest, 묶인 값을 펼쳐 사용하는 자리이면Spread이다.
Rest Operator와Spread Operator는 같은 기호를 쓰지만,Rest는 여러 값을 모으고Spread는 묶인 값을 펼친다.
구조분해할당은 배열이나 객체 안에 들어 있는 값을 꺼내서 변수에 바로 담는 문법이다.
구조를 분해해서 값을 할당한다는 의미이다.
여기서 구조는 배열이나 객체처럼 여러 값이 묶여 있는 형태를 말한다.
배열 구조분해할당은 순서를 기준으로 값을 꺼내고, 객체 구조분해할당은key이름을 기준으로 값을 꺼낸다.
배열 구조분해할당
배열은 값들이 순서대로 들어 있는 구조이다.
그래서 배열 구조분해할당은 왼쪽 변수의 순서와 오른쪽 배열 값의 순서를 맞춰서 값을 꺼낸다.// destructuring-array-basic.js const numbers = [10, 20, 30]; // 숫자 배열 생성 const [first, second] = numbers; // 배열의 첫 번째, 두 번째 값을 변수에 저장 console.log(first); // 첫 번째 값 출력 console.log(second); // 두 번째 값 출력// 출력결과 // 10 // 20배열 구조분해할당에서는 변수 이름보다 위치가 중요하다.
첫 번째 자리에 적힌 변수가 첫 번째 값을 받고, 두 번째 자리에 적힌 변수가 두 번째 값을 받는다.
필요 없는 값은 건너뛸 수 있다.// destructuring-array-skip.js const colors = ["red", "green", "blue"]; // 색상 배열 생성 const [firstColor, , thirdColor] = colors; // 두 번째 값은 건너뛰고 첫 번째와 세 번째 값만 저장 console.log(firstColor); // 첫 번째 색상 출력 console.log(thirdColor); // 세 번째 색상 출력// 출력결과 // red // blue
배열 구조분해할당과 Rest Operator
배열 구조분해할당은
Rest Operator와 함께 사용할 수 있다.
앞의 값은 각각 꺼내고, 나머지 값은 배열로 묶고 싶을 때 사용한다.// destructuring-array-rest.js const scores = [100, 90, 80, 70, 60]; // 점수 배열 생성 const [firstScore, secondScore, ...otherScores] = scores; // 앞의 두 값은 따로 받고 나머지는 배열로 묶음 console.log(firstScore); // 첫 번째 점수 출력 console.log(secondScore); // 두 번째 점수 출력 console.log(otherScores); // 나머지 점수 배열 출력// 출력결과 // 100 // 90 // [80, 70, 60]배열 구조분해할당에서
Rest Operator는 앞에서 꺼내고 남은 요소들을 새 배열로 묶는다.
객체 구조분해할당
객체는 배열과 다르게 순서보다
key이름이 중요하다.
객체는{ key: value }형태로 값을 저장한다.// destructuring-object-basic.js const user = { nickname: "kim", // 닉네임 프로퍼티 age: 20, // 나이 프로퍼티 city: "seoul" // 도시 프로퍼티 }; const { nickname, age } = user; // key 이름과 같은 변수로 값 꺼내기 console.log(nickname); // nickname 값 출력 console.log(age); // age 값 출력// 출력결과 // kim // 20객체 구조분해할당에서는 위치가 기준이 아니다.
객체 안에서nickname이 먼저 있든 뒤에 있든,key이름이 맞으면 값을 꺼낼 수 있다.
객체의key와 다른 변수명으로 값을 꺼낼 수도 있다.// destructuring-object-rename.js const user = { nickname: "kim", // 닉네임 프로퍼티 age: 20 // 나이 프로퍼티 }; const { nickname: userNickname, age: userAge } = user; // key 이름과 다른 변수명으로 값 꺼내기 console.log(userNickname); // 바꾼 변수명으로 닉네임 출력 console.log(userAge); // 바꾼 변수명으로 나이 출력// 출력결과 // kim // 20
nickname: userNickname에서 왼쪽의nickname은 객체의key이름이고, 오른쪽의userNickname은 새로 만드는 변수 이름이다.
객체 구조분해할당에서key: 새변수명형태를 쓰면, 실제로 만들어지는 변수는 오른쪽의 새 변수명이다.
객체 구조분해할당과 Rest Operator
객체 구조분해할당에서도
Rest Operator를 사용할 수 있다.
배열에서는 남은 요소가 배열로 묶였지만, 객체에서는 남은 프로퍼티가 새 객체로 묶인다.// destructuring-object-rest.js const user = { nickname: "kim", // 닉네임 프로퍼티 age: 20, // 나이 프로퍼티 city: "seoul", // 도시 프로퍼티 job: "developer" // 직업 프로퍼티 }; const { nickname, ...profile } = user; // nickname은 따로 받고 나머지는 객체로 묶음 console.log(nickname); // 따로 꺼낸 nickname 출력 console.log(profile); // 나머지 프로퍼티 객체 출력// 출력결과 // kim // { age: 20, city: 'seoul', job: 'developer' }배열 구조분해할당에서
Rest Operator는 남은 요소를 배열로 묶는다.
객체 구조분해할당에서Rest Operator는 남은 프로퍼티를 객체로 묶는다.
구조분해할당 핵심 정리
- 구조분해할당은 묶여 있는 값을 분해해서 변수에 저장하는 문법이다.
- 배열 구조분해할당은 순서를 기준으로 값을 꺼낸다.
- 객체 구조분해할당은
key이름을 기준으로 값을 꺼낸다.- 객체에서
key: 새변수명형태로 다른 변수명을 사용할 수 있다.- 구조분해할당에서
Rest Operator는 마지막에 작성해야 한다.구조분해할당을 읽을 때는 배열인지 객체인지 먼저 보고, 배열이면 순서 기준, 객체면
key이름 기준으로 값을 꺼낸다고 판단해야 한다.
JavaScript에서 배열은 여러 값을 순서대로 담아 두는 구조이다.
배열에 값이 여러 개 들어 있으면 보통 그 값을 하나씩 꺼내서 처리해야 한다.
배열 처리 메서드는 이런 반복 작업을 더 읽기 쉽게 작성하기 위한 도구이다.
forEach(),map(),reduce(),filter()는ES6에서 새로 추가된 문법은 아니다.
하지만ES6문법인 화살표 함수와 함께 자주 사용되고, 현대JavaScript코드에서 배열 데이터를 처리할 때 기본처럼 쓰인다.
그래서 이 정리본에서는ES6흐름과 연결해서 함께 정리한다.
배열 처리 메서드는 배열 안의 요소를 하나씩 꺼내 처리하는 흐름을 더 분명하게 작성하기 위한 도구이다.
대표적으로forEach(),map(),reduce(),filter()가 있다.
callback 함수
배열 처리 메서드를 이해하려면 먼저
callback함수를 알아야 한다.
callback함수는 다른 함수에게 전달되어 나중에 실행되는 함수이다.
배열 메서드에서는callback함수가 배열 요소마다 반복 실행된다.
대부분의 배열 메서드에서 자주 쓰는callback함수의 매개변수는 현재 요소, 인덱스, 원본 배열이다.
모든 예제에서 세 매개변수를 전부 사용할 필요는 없고, 필요한 것만 사용하면 된다.
forEach는 배열 요소마다 함수를 실행한다
forEach()는 배열의 요소를 하나씩 꺼내면서callback함수를 실행하는 메서드이다.
처리 결과를 모아 새 배열을 만들어 주지는 않는다.// array-foreach-basic.js const fruits = ["apple", "banana", "orange"]; // 과일 이름 배열 fruits.forEach((fruit, index) => { // 배열 요소와 인덱스를 하나씩 꺼내 실행 console.log(`${index}번 과일: ${fruit}`); // 인덱스와 값 출력 });// 출력결과 // 0번 과일: apple // 1번 과일: banana // 2번 과일: orange
forEach()는 배열 요소마다 어떤 동작을 실행할 때 사용하고, 실행 결과를 새 배열로 반환하지 않는다.
map은 배열 요소를 변환한 결과로 새 배열을 만든다
map()은 배열의 요소를 하나씩 처리한 뒤, 각 처리 결과를 모아 새 배열을 만드는 메서드이다.// array-map-basic.js const numbers = [1, 2, 3, 4]; // 숫자 배열 const doubledNumbers = numbers.map((number) => { // 각 요소를 처리해 새 배열 생성 return number * 2; // 현재 숫자를 2배로 계산한 값 반환 }); console.log(doubledNumbers); // 새 배열 출력 console.log(numbers); // 원본 배열 확인// 출력결과 // [2, 4, 6, 8] // [1, 2, 3, 4]
map()은 반환값을 하나씩 모아 새 배열을 만든다.
원본 배열은 그대로 남아 있다.
map()의 핵심은 원본 배열을 직접 바꾸는 것이 아니라, 배열 요소를 변환한 결과로 같은 길이의 새 배열을 만든다는 점이다.
forEach와 map은 반환값이 다르다
forEach()와map()은 둘 다 배열 요소마다 함수를 실행한다.
하지만forEach()는 새 배열을 반환하지 않고,map()은 새 배열을 반환한다.// array-foreach-map-difference.js const numbers = [1, 2, 3]; // 숫자 배열 const forEachResult = numbers.forEach((number) => { // 각 요소마다 실행 return number * 2; // 반환해도 새 배열로 모이지 않음 }); const mapResult = numbers.map((number) => { // 각 요소마다 실행 return number * 2; // 반환값을 모아 새 배열 생성 }); console.log(forEachResult); // forEach 결과 확인 console.log(mapResult); // map 결과 확인// 출력결과 // undefined // [2, 4, 6]
forEach()는 반복 실행용이고,map()은 변환 결과로 새 배열을 만들기 위한 메서드이다.
reduce와 filter
reduce()는 배열의 여러 값을 하나의 결과값으로 줄이는 메서드이다.
배열의 요소를 하나씩 처리하면서 누적값을 만들고, 마지막에 하나의 결과를 반환한다.// array-reduce-sum.js const numbers = [10, 20, 30]; // 숫자 배열 const sum = numbers.reduce((total, number) => { // total은 누적값, number는 현재값 return total + number; // 누적값에 현재값 더하기 }, 0); // 초기 누적값은 0 console.log(sum); // 합계 출력// 출력결과 // 60
reduce()의 두 번째 인자는 초깃값이다.
위 예제에서는 초깃값을0으로 정했기 때문에 누적값total은0에서 시작한다.
그다음 배열의 첫 번째 값10, 두 번째 값20, 세 번째 값30을 차례대로 더해 최종 결과60을 만든다.
초깃값을 생략하면 배열의0번째 요소가 처음 누적값으로 사용된다.
예를 들어[10, 20, 30]에서 초깃값을 생략하면10이 처음 누적값이 되고, 그다음 값인20부터 계산이 시작된다.
초보자 단계에서는 누적 흐름을 예측하기 쉽도록 초깃값을 직접 적는 편이 좋다.
reduce()는 초깃값을 명시하면 누적 계산이 어디서 시작되는지 분명해진다.
filter()는 배열 요소 중 조건을 만족하는 값만 골라 새 배열을 만드는 메서드이다.// array-filter-basic.js const numbers = [1, 2, 3, 4, 5, 6]; // 숫자 배열 const evenNumbers = numbers.filter((number) => { // 조건에 맞는 요소만 새 배열로 생성 return number % 2 === 0; // 짝수이면 true }); console.log(evenNumbers); // 짝수 배열 출력// 출력결과 // [2, 4, 6]
filter()와map()은 이어서 사용할 수 있다.// array-filter-map-chain.js const numbers = [1, 2, 3, 4, 5, 6]; // 숫자 배열 const result = numbers .filter(number => number % 2 === 0) // 짝수만 고르기 .map(number => number * 10); // 고른 값들을 10배로 변환 console.log(result); // 최종 배열 출력// 출력결과 // [20, 40, 60]
filter()는 조건에 맞는 요소를 고르고,map()은 고른 요소를 다른 값으로 변환한다.
배열 처리 메서드 핵심 정리
forEach(),map(),reduce(),filter()는ES6자체에서 새로 추가된 문법은 아니지만 현대JavaScript배열 처리에서 자주 사용한다.forEach()는 배열 요소마다 함수를 실행한다.forEach()는 새 배열을 반환하지 않는다.map()은 배열 요소를 변환해서 새 배열을 반환한다.reduce()는 배열 요소를 누적해서 하나의 결과값으로 만든다.reduce()의 두 번째 인자는 초깃값이다.reduce()에서 초깃값을 생략하면 배열의 첫 번째 요소가 처음 누적값으로 사용된다.filter()는 조건을 만족하는 요소만 골라 새 배열을 만든다.filter()와map()은 이어서 사용할 수 있다.배열 처리 메서드를 사용할 때는 “반복 실행인지, 새 배열 만들기인지, 하나의 값으로 줄이기인지, 조건으로 거르기인지”를 먼저 판단해야 한다.
Promise는 비동기 작업의 결과를 나중에 받기 위해 사용하는 객체이다.
여기서 비동기 작업은 코드가 실행되자마자 결과가 바로 나오지 않는 작업을 말한다.
예를 들어 서버에서 데이터를 받아 오거나, 일정 시간이 지난 뒤 실행되는 작업은 결과가 바로 준비되지 않는다.
이런 작업은 “지금 당장 결과는 없지만, 나중에 성공하거나 실패할 수 있는 작업”으로 봐야 한다.
Promise는 이처럼 나중에 끝나는 작업을 하나의 객체로 감싸서 다룰 수 있게 해 준다.
작업이 성공하면 성공 결과를 전달하고, 작업이 실패하면 실패 이유를 전달한다.
Promise는 비동기 작업의 성공 또는 실패 결과를 나중에 처리하기 위한 약속 객체이다.
즉, 결과가 아직 준비되지 않은 작업을 정리된 흐름으로 다루기 위해 사용한다.
Promise는 바로 값이 아니라 나중의 결과를 담는 약속이다
Promise를 처음 볼 때 가장 헷갈리는 부분은Promise가 결과값 그 자체가 아니라는 점이다.
Promise는 “나중에 결과가 생길 작업”을 표현한다.
예를 들어 서버에 사용자 정보를 요청했다고 생각해 보자.
요청을 보낸 순간에는 아직 사용자 정보가 도착하지 않았다.
네트워크 상황에 따라 조금 뒤에 도착할 수도 있고, 오류 때문에 실패할 수도 있다.
이때Promise는 이런 상태를 표현한다.
지금 당장은 결과가 없지만, 시간이 지나면 성공 결과가 생기거나 실패 이유가 생긴다.
쉽게 말하면 아래처럼 볼 수 있다.
- 지금 결과가 없지만 작업이 진행 중이면
Promise가 대기 상태를 가진다.- 작업이 성공하면 성공 결과를 가진다.
- 작업이 실패하면 실패 이유를 가진다.
그래서
Promise는 비동기 작업을 “성공할 수도 있고 실패할 수도 있는 하나의 흐름”으로 다루게 해 준다.
setTimeout만 사용하면 작업 순서가 기대와 다를 수 있다
Promise가 필요한 이유를 이해하려면 먼저 비동기 작업의 실행 순서를 봐야 한다.
setTimeout()은 일정 시간이 지난 뒤 코드를 실행하는 비동기 처리 함수이다.
그래서setTimeout()안의 코드가 끝날 때까지 전체 코드가 멈춰서 기다리는 것이 아니다.
아래 예제는setTimeout()만 사용했을 때의 흐름이다.// exam26.html <h1>setTimeout() 비동기 처리 함수라서...</h1> <script> function work() { console.log("작업 시작"); // ① 먼저 실행 setTimeout(() => { console.log("작업 완료"); // ③ 2초 뒤 실행 }, 2000); console.log("다음 작업"); // ② 기다리지 않고 바로 실행 } work(); // 함수 실행 </script>// 출력결과 // 작업 시작 // 다음 작업 // 작업 완료코드를 위에서 아래로 보면
"작업 완료"가"다음 작업"보다 먼저 나올 것처럼 보일 수 있다.
하지만setTimeout()은 비동기 처리 함수이기 때문에2초를 기다리는 동안 다음 코드가 먼저 실행된다.
그래서 출력 순서는"작업 시작" → "다음 작업" → "작업 완료"가 된다.
이 흐름이 항상 나쁜 것은 아니다.
하지만 “작업이 완료된 다음에 다음 작업을 실행해야 하는 상황”이라면 이 순서는 문제가 될 수 있다.
예를 들어 서버에서 데이터를 받아 온 뒤 화면에 출력해야 하는데, 데이터가 오기 전에 화면 출력 코드가 먼저 실행되면 원하는 결과가 나오지 않는다.
이럴 때Promise를 사용하면 비동기 작업이 끝난 뒤 다음 흐름을 연결할 수 있다.// exam26_1.html <h2>Promise 객체를 사용하는 이유 : 비동기 작업 순서를 깔끔하게 처리하기 위해</h2> <h3>Promise는 지금 바로 결과는 없지만, 작업이 끝나면 결과를 알려주겠다는 객체</h3> <script> function work() { return new Promise((resolve, reject) => { // Promise 객체 반환 console.log("작업 시작"); // ① 먼저 실행 setTimeout(() => { resolve("작업 완료"); // ② 2초 뒤 성공 결과 전달 }, 2000); }); } work() .then((result) => { console.log(result); // ② resolve 값 출력 }) .finally(() => { console.log("다음 작업"); // ③ 작업 완료 후 마지막 실행 }); </script>// 출력결과 // 작업 시작 // 작업 완료 // 다음 작업이번에는
work()가Promise를 반환한다.
setTimeout()안에서resolve("작업 완료")가 호출되면 작업이 성공 상태가 되고,.then()에서 그 결과를 받는다.
그리고.finally()에서 마지막 작업을 실행한다.
이렇게 작성하면"작업 시작" → "작업 완료" → "다음 작업"순서가 된다.
Promise는 비동기 작업의 성공과 실패를 다루는 객체이면서, 비동기 작업이 끝난 뒤 다음 흐름을 깔끔하게 연결하기 위한 도구이다.
Promise는 세 가지 상태를 가진다
Promise는 작업 진행 상황에 따라 상태가 바뀐다.
대표 상태는Pending,Fulfilled,Rejected이다.
Pending은 대기 상태이다.
아직 작업이 끝나지 않았고, 성공인지 실패인지 결정되지 않은 상태이다.
Fulfilled는 이행 상태이다.
작업이 성공적으로 끝났고, 성공 결과가 준비된 상태이다.
Rejected는 거부 상태이다.
작업이 실패했고, 실패 이유가 준비된 상태이다.
이 세 상태를 정리하면 아래와 같다.
Pending은 아직 작업 결과가 결정되지 않은 상태이다.Fulfilled는 작업이 성공한 상태이다.Rejected는 작업이 실패한 상태이다.
Promise는 처음에는Pending상태로 시작하고, 작업 결과에 따라Fulfilled또는Rejected상태로 바뀐다.
한 번 성공하거나 실패한Promise는 다시 대기 상태로 돌아가지 않는다.
Promise는 처음에는Pending상태에서 기다리고, 작업이 성공하면Fulfilled, 실패하면Rejected상태로 바뀐다.
resolve()는 성공 흐름으로 보내고,reject()는 실패 흐름으로 보낸다.
resolve는 성공 결과를 전달한다
Promise를 만들 때는new Promise()를 사용한다.
그리고 그 안에 함수를 전달한다.
이 함수 안에서 작업이 성공하면resolve()를 호출한다.
resolve는 작업이 성공했을 때 결과를 전달하는 함수이다.
resolve()안에 넣은 값은 나중에.then()에서 받을 수 있다.
아래 예제는 성공하는Promise이다.// promise-resolve-basic.js const promise = new Promise((resolve, reject) => { // Promise 객체 생성 resolve("작업 성공"); // 성공 결과 전달 }); promise.then((result) => { // 성공 결과 받기 console.log(result); // 전달받은 결과 출력 });// 출력결과 // 작업 성공이 코드에서
new Promise()안의 함수가 실행된다.
그 안에서resolve("작업 성공")을 호출한다.
그러면 이Promise는 성공 상태가 된다.
그다음.then()이 실행된다.
resolve()에 넣었던"작업 성공"값이.then()의result로 전달된다.
그래서"작업 성공"이 출력된다.
정리하면resolve()는 성공 결과를 넘겨 주고,.then()은 그 성공 결과를 받아 처리한다.
reject는 실패 이유를 전달한다
작업이 항상 성공하는 것은 아니다.
서버 요청이 실패하거나, 필요한 데이터가 없거나, 조건이 맞지 않으면 실패할 수 있다.
이때 사용하는 함수가reject이다.
reject는 작업이 실패했을 때 실패 이유를 전달하는 함수이다.
reject()안에 넣은 값은 나중에.catch()에서 받을 수 있다.
아래 예제는 실패하는Promise이다.// promise-reject-basic.js const promise = new Promise((resolve, reject) => { // Promise 객체 생성 reject("작업 실패"); // 실패 이유 전달 }); promise.catch((error) => { // 실패 이유 받기 console.log(error); // 전달받은 오류 출력 });// 출력결과 // 작업 실패이 코드에서
reject("작업 실패")가 실행된다.
그러면 이Promise는 실패 상태가 된다.
실패 상태가 되면.then()이 아니라.catch()에서 실패 이유를 처리한다.
reject()에 넣었던"작업 실패"값이.catch()의error로 전달된다.
그래서"작업 실패"가 출력된다.
정리하면reject()는 실패 이유를 넘겨 주고,.catch()는 그 실패 이유를 받아 처리한다.
then, catch, finally는 Promise 결과를 처리한다
Promise의 결과를 처리할 때는.then(),.catch(),.finally()를 사용한다.
이 세 메서드는 각각 역할이 다르다.
.then()은Promise가 성공했을 때 실행된다.
성공 결과를 받아서 다음 작업을 처리할 수 있다.
.catch()는Promise가 실패했을 때 실행된다.
실패 이유를 받아서 오류 처리 흐름을 작성할 수 있다.
.finally()는 성공하든 실패하든 마지막에 실행된다.
Promise.prototype.finally()는Promise흐름에서 자주 쓰이지만,ES6이후 표준에서 추가된 메서드이다.
여기서는 성공과 실패 후 공통 마무리 흐름을 이해하기 위해 함께 정리한다.
성공 결과나 실패 이유와 상관없이 공통으로 처리해야 하는 작업에 사용한다.
예를 들어 로딩 표시를 끄거나, 마무리 로그를 남기는 작업이 여기에 해당한다.
아래 예제는 성공 흐름에서.then()과.finally()가 실행되는 코드이다.// promise-then-finally.js const promise = new Promise((resolve, reject) => { // Promise 객체 생성 resolve("데이터 조회 성공"); // 성공 결과 전달 }); promise .then((result) => { // 성공했을 때 실행 console.log(result); // 성공 결과 출력 }) .catch((error) => { // 실패했을 때 실행 console.log(error); // 실패 이유 출력 }) .finally(() => { // 성공과 실패에 상관없이 실행 console.log("작업 종료"); // 마무리 문장 출력 });// 출력결과 // 데이터 조회 성공 // 작업 종료
resolve("데이터 조회 성공")이 실행되었기 때문에Promise는 성공 상태가 된다.
그래서.then()이 실행되고,"데이터 조회 성공"이 출력된다.
실패한 것이 아니므로.catch()는 실행되지 않는다.
하지만.finally()는 성공과 실패에 상관없이 실행된다.
그래서 마지막에"작업 종료"가 출력된다.
이번에는 실패 흐름을 보면 된다.// promise-catch-finally.js const promise = new Promise((resolve, reject) => { // Promise 객체 생성 reject("데이터 조회 실패"); // 실패 이유 전달 }); promise .then((result) => { // 성공했을 때 실행 console.log(result); // 성공 결과 출력 }) .catch((error) => { // 실패했을 때 실행 console.log(error); // 실패 이유 출력 }) .finally(() => { // 성공과 실패에 상관없이 실행 console.log("작업 종료"); // 마무리 문장 출력 });// 출력결과 // 데이터 조회 실패 // 작업 종료
reject("데이터 조회 실패")가 실행되었기 때문에Promise는 실패 상태가 된다.
그래서.then()은 실행되지 않고,.catch()가 실행된다.
그리고.finally()는 실패한 경우에도 실행된다.
그래서"데이터 조회 실패"다음에"작업 종료"가 출력된다.
.then()은 성공 처리,.catch()는 실패 처리,.finally()는 성공과 실패에 상관없는 마무리 처리에 사용한다.
Promise 체이닝은 작업을 순서대로 이어 붙이는 방식이다
Promise는.then()을 이어 붙여서 다음 작업으로 결과를 넘길 수 있다.
이것을Promise체이닝이라고 한다.
체이닝은 메서드를 사슬처럼 계속 이어서 호출하는 방식이다.
Promise체이닝이 필요한 이유는 비동기 작업의 결과를 다음 작업에서 계속 사용해야 할 때가 있기 때문이다.
예를 들어 첫 번째 작업에서 숫자를 받고, 두 번째 작업에서 그 숫자에2를 곱하고, 세 번째 작업에서 다시10을 더할 수 있다.
아래 예제는.then()을 이어 붙이는 기본 흐름이다.// promise-chaining-basic.js const promise = new Promise((resolve, reject) => { // Promise 객체 생성 resolve(10); // 첫 번째 성공 결과 전달 }); promise .then((number) => { // 첫 번째 then console.log(number); // 10 출력 return number * 2; // 다음 then으로 20 전달 }) .then((number) => { // 두 번째 then console.log(number); // 20 출력 return number + 10; // 다음 then으로 30 전달 }) .then((number) => { // 세 번째 then console.log(number); // 30 출력 });// 출력결과 // 10 // 20 // 30첫 번째
.then()은resolve(10)에서 전달한10을 받는다.
그 안에서number * 2를return했기 때문에 다음.then()으로20이 전달된다.
두 번째.then()은20을 받아 출력하고,number + 10을return한다.
그래서 세 번째.then()으로30이 전달된다.
이처럼.then()안에서 값을return하면, 그 값이 다음.then()으로 넘어간다.
Promise체이닝은 이전 작업의 결과를 다음 작업으로 넘기면서 비동기 흐름을 순서대로 연결하는 방식이다.
Promise 안의 작업은 바로 실행된다
초보자가 헷갈리기 쉬운 부분이 있다.
Promise객체를 만들기만 해도new Promise()안에 전달한 함수는 바로 실행된다.
.then()을 붙였을 때 작업이 처음 시작되는 것이 아니다.
Promise를 생성하는 순간 내부 함수가 먼저 실행되고,.then()은 그 결과를 나중에 처리하는 역할을 한다.
아래 예제로 확인할 수 있다.// promise-executor-run.js const promise = new Promise((resolve, reject) => { // Promise 생성과 동시에 실행 console.log("Promise 내부 실행"); // 먼저 출력 resolve("성공 결과"); // 성공 결과 전달 }); console.log("then 등록 전"); // Promise 생성 후 출력 promise.then((result) => { // 성공 결과 처리 console.log(result); // 성공 결과 출력 });// 출력결과 // Promise 내부 실행 // then 등록 전 // 성공 결과이 코드에서
new Promise()안의console.log("Promise 내부 실행")이 먼저 실행된다.
그다음"then 등록 전"이 출력된다.
마지막으로.then()에서"성공 결과"가 출력된다.
즉,Promise는 생성되는 순간 내부 작업이 실행된다.
.then()은 그 작업이 성공했을 때 결과를 받아 처리하는 흐름이다.
Promise 유틸리티는 여러 Promise의 결과를 기준에 맞게 처리한다
Promise.all(),Promise.allSettled(),Promise.race(),Promise.any()는 여러Promise를 함께 다룰 때 사용하는 메서드이다.
이 메서드들은 모두 여러Promise를 다루는 흐름에서 함께 자주 쓰이지만, 모두ES6에서 한 번에 추가된 문법은 아니다.
Promise.all()과Promise.race()는ES6부터 사용할 수 있고,Promise.allSettled()와Promise.any()는 이후 표준에서 추가된 메서드이다.
여기서는 여러Promise결과를 비교해서 이해하기 위해 함께 정리한다.
여기서 중요한 점은 이 메서드들이 비동기 작업을 직접 새로 시작하는 도구가 아니라는 점이다.
이미 만들어진 여러Promise를 받아서, 각각의 기준에 맞게 성공 결과나 실패 결과를 처리한다.
예제에서 사용하는Promise.resolve()는 이미 성공한Promise를 만드는 메서드이다.
Promise.reject()는 이미 실패한Promise를 만드는 메서드이다.
예제에서는 여러Promise의 성공과 실패 흐름을 짧게 보여 주기 위해 사용한다.
Promise유틸리티는 여러 작업을 직접 실행하는 문법이 아니라, 전달받은 여러Promise의 결과를 어떤 기준으로 처리할지 정하는 메서드이다.
Promise.all은 여러 Promise가 모두 성공해야 성공한다
여러 비동기 작업의 결과를 한 번에 기다리고, 모든 작업이 성공했을 때 결과를 모아야 할 때가 있다.
이때Promise.all()을 사용할 수 있다.
Promise.all()은 여러Promise를 배열로 받는다.
그리고 모든Promise가 성공하면, 각 성공 결과를 배열로 모아.then()에 전달한다.
이때 결과 배열은 먼저 끝난 순서가 아니라,Promise.all()에 전달한 배열의 순서를 따른다.
아래 예제는 세 작업이 모두 성공하는 경우이다.// promise-all-basic.js const promise1 = Promise.resolve("첫 번째 성공"); // 이미 성공한 Promise const promise2 = Promise.resolve("두 번째 성공"); // 이미 성공한 Promise const promise3 = Promise.resolve("세 번째 성공"); // 이미 성공한 Promise Promise.all([promise1, promise2, promise3]) // 모든 Promise가 성공해야 성공 .then((results) => { // 성공 결과 배열 받기 console.log(results); // 결과 배열 출력 });// 출력결과 // ['첫 번째 성공', '두 번째 성공', '세 번째 성공']세
Promise가 모두 성공했기 때문에.then()이 실행된다.
results에는 각Promise의 성공 결과가 배열로 들어간다.
하지만 하나라도 실패하면Promise.all()은 실패한다.// promise-all-reject.js const promise1 = Promise.resolve("첫 번째 성공"); // 성공 Promise const promise2 = Promise.reject("두 번째 실패"); // 실패 Promise const promise3 = Promise.resolve("세 번째 성공"); // 성공 Promise Promise.all([promise1, promise2, promise3]) // 하나라도 실패하면 실패 .then((results) => { // 모두 성공해야 실행 console.log(results); // 실행되지 않음 }) .catch((error) => { // 실패 처리 console.log(error); // 실패 이유 출력 });// 출력결과 // 두 번째 실패
promise2가 실패했기 때문에Promise.all()은 전체 실패로 처리된다.
그래서.then()은 실행되지 않고.catch()가 실행된다.
다만Promise.all()이 실패 흐름으로 넘어간다고 해서 이미 시작된 다른 작업이 자동으로 취소된다는 뜻은 아니다.
여기서는 결과 처리 흐름이 실패로 넘어간다고 이해하면 된다.
Promise.all()은 여러 작업이 모두 성공해야 다음 단계로 넘어갈 수 있을 때 사용한다.
하나라도 실패하면 전체가 실패 흐름으로 넘어가지만, 이미 시작된 다른Promise작업 자체를 자동으로 멈추는 것은 아니다.
Promise.allSettled는 성공과 실패를 모두 기다린다
Promise.allSettled()는 여러Promise가 성공하든 실패하든 모두 끝날 때까지 기다린다.
여기서settled는 성공 또는 실패로 결과가 결정된 상태를 의미한다.
Promise.all()은 하나라도 실패하면 실패 흐름으로 넘어간다.
반면Promise.allSettled()는 실패한 작업이 있어도 나머지 작업까지 모두 끝난 뒤 결과를 모아 준다.
아래 예제는 성공과 실패가 섞여 있는 경우이다.// promise-allsettled-basic.js const promise1 = Promise.resolve("첫 번째 성공"); // 성공 Promise const promise2 = Promise.reject("두 번째 실패"); // 실패 Promise const promise3 = Promise.resolve("세 번째 성공"); // 성공 Promise Promise.allSettled([promise1, promise2, promise3]) // 성공과 실패를 모두 기다림 .then((results) => { // 모든 결과 받기 console.log(results); // 결과 상태 배열 출력 });// 출력결과 // [ // { status: 'fulfilled', value: '첫 번째 성공' }, // { status: 'rejected', reason: '두 번째 실패' }, // { status: 'fulfilled', value: '세 번째 성공' } // ]결과 배열에는 각
Promise의 상태가 들어간다.
성공한 작업은status가"fulfilled"이고, 성공 값은value에 들어간다.
실패한 작업은status가"rejected"이고, 실패 이유는reason에 들어간다.
즉,Promise.allSettled()는 중간에 실패가 있어도 전체 작업 결과를 끝까지 확인할 수 있다.
Promise.race는 가장 먼저 끝난 Promise의 결과를 사용한다
Promise.race()는 여러Promise중 가장 먼저 결과가 결정된Promise의 결과를 사용한다.
여기서 결과가 결정된다는 말은 성공 또는 실패 중 하나로 끝났다는 뜻이다.
race는 경주라는 뜻이다.
이미 만들어진 여러Promise중 가장 먼저 끝난 작업의 결과를 사용하는 방식이라고 이해하면 된다.
아래 예제는 가장 먼저 성공한 작업의 결과가 사용되는 경우이다.// promise-race-basic.js const slowPromise = new Promise((resolve, reject) => { // 느린 Promise setTimeout(() => resolve("느린 작업 성공"), 2000); // 2초 뒤 성공 }); const fastPromise = new Promise((resolve, reject) => { // 빠른 Promise setTimeout(() => resolve("빠른 작업 성공"), 1000); // 1초 뒤 성공 }); Promise.race([slowPromise, fastPromise]) // 가장 먼저 끝난 Promise 사용 .then((result) => { // 먼저 성공한 결과 받기 console.log(result); // 결과 출력 });// 출력결과 // 빠른 작업 성공
fastPromise는1초 뒤에 성공하고,slowPromise는2초 뒤에 성공한다.
그래서 가장 먼저 끝난fastPromise의 결과인"빠른 작업 성공"이 출력된다.
중요한 점은Promise.race()가 가장 먼저 성공한 것만 보는 것이 아니라는 점이다.
가장 먼저 끝난 작업이 실패라면 실패 흐름으로 넘어간다.
또 하나 더 주의할 점이 있다.
Promise.race()가 가장 먼저 끝난 결과를 선택한다고 해서 나머지Promise작업이 자동으로 취소되는 것은 아니다.
이미 시작된 작업은 계속 진행될 수 있고,race()는 그중 가장 먼저 결정된 결과를 최종 처리 흐름으로 사용하는 것이다.
Promise.race()는 가장 먼저 끝난 결과를 선택하지만, 선택되지 않은 다른 작업을 자동으로 멈추는 문법은 아니다.
Promise.any는 가장 먼저 성공한 Promise의 결과를 사용한다
Promise.any()는 여러Promise중 가장 먼저 성공한 결과를 사용한다.
Promise.race()와 비슷해 보이지만 기준이 다르다.
Promise.race()는 성공이든 실패든 가장 먼저 끝난 작업을 기준으로 한다.
반면Promise.any()는 실패는 최종 성공 결과로 보지 않고, 가장 먼저 성공한 결과를 찾는다.
아래 예제는 먼저 실패가 나오지만, 나중에 성공한 결과가 사용되는 경우이다.// promise-any-basic.js const failPromise = new Promise((resolve, reject) => { // 실패 Promise setTimeout(() => reject("첫 번째 실패"), 1000); // 1초 뒤 실패 }); const successPromise = new Promise((resolve, reject) => { // 성공 Promise setTimeout(() => resolve("두 번째 성공"), 2000); // 2초 뒤 성공 }); Promise.any([failPromise, successPromise]) // 가장 먼저 성공한 Promise 사용 .then((result) => { // 성공 결과 받기 console.log(result); // 결과 출력 });// 출력결과 // 두 번째 성공
failPromise가 먼저 끝나지만 실패 결과이다.
Promise.any()는 이 실패 결과를 최종 결과로 삼지 않는다.
그다음successPromise가 성공하면 그 성공 결과를 사용한다.
만약 모든Promise가 실패하면Promise.any()도 실패한다.
이때는 모든 작업이 실패했다는 의미로AggregateError가 발생한다.
AggregateError는 여러 실패 이유를 한 번에 담을 수 있는 오류 객체라고 이해하면 된다.
Promise.race()는 성공과 실패를 가리지 않고 가장 먼저 완료된Promise의 결과를 사용한다.
반면Promise.any()는 실패를 건너뛰고 가장 먼저 성공한Promise의 결과를 사용한다.
Promise 유틸리티는 성공 조건과 완료 기준이 다르다
Promise.all(),Promise.allSettled(),Promise.race(),Promise.any()는 여러Promise를 함께 다룰 때 사용한다.
하지만 언제 성공으로 볼지, 언제 결과를 넘길지가 다르다.
차이를 짧게 정리하면 아래와 같다.
Promise.all()은 모든 작업이 성공해야 성공한다.Promise.allSettled()는 성공과 실패에 상관없이 모두 끝날 때까지 기다린다.Promise.race()는 성공이든 실패든 가장 먼저 끝난 작업의 결과를 사용한다.Promise.any()는 가장 먼저 성공한 작업의 결과를 사용한다.여러
Promise를 함께 다룰 때는 “모두 성공해야 하는지, 모두 끝나기만 하면 되는지, 가장 빠른 결과가 필요한지, 가장 빠른 성공이 필요한지”를 먼저 판단해야 한다.
이 기준을 잡으면Promise유틸리티를 더 쉽게 선택할 수 있다.
Promise에서 async/await로 이어진다
Promise를 사용하면 비동기 작업의 성공과 실패를 정리해서 처리할 수 있다.
하지만.then()을 계속 이어 붙이면 코드가 길어질 수 있다.
특히 앞 작업의 결과를 다음 작업에서 계속 사용해야 하면 체이닝 구조가 길어지고, 흐름을 위에서 아래로 읽기 어려워질 수 있다.
이 문제를 더 읽기 쉽게 풀기 위해 사용하는 방식이async/await이다.
async/await은Promise를 없애는 문법이 아니다.
Promise기반 비동기 코드를 동기 코드처럼 위에서 아래로 읽기 쉽게 작성하게 도와주는 문법이다.
이 흐름은 다음 주제에서 더 자세히 정리한다.
여기서는Promise가 비동기 작업의 결과를 표현하고,async/await은 그Promise흐름을 더 읽기 쉽게 쓰는 방식이라는 정도로 이해하면 된다.
Promise 핵심 정리
Promise는 비동기 작업의 결과를 나중에 처리하기 위한 객체이다.
작업이 성공하면 성공 결과를 전달하고, 실패하면 실패 이유를 전달한다.
핵심은 아래처럼 정리할 수 있다.
Promise는 비동기 작업의 성공 또는 실패 결과를 나중에 처리하기 위한 객체이다.Promise는 비동기 작업이 끝난 뒤 다음 흐름을 연결할 때 사용한다.Promise는 처음에Pending상태로 시작한다.- 작업이 성공하면
Fulfilled상태가 된다.- 작업이 실패하면
Rejected상태가 된다.resolve()는 성공 결과를 전달한다.reject()는 실패 이유를 전달한다..then()은 성공 결과를 처리한다..catch()는 실패 이유를 처리한다..finally()는 성공과 실패에 상관없이 마지막에 실행되며,ES6이후 표준에서 추가된 메서드이다..then()을 이어 붙이면 이전 결과를 다음 작업으로 넘길 수 있다.Promise를 생성하면 내부 작업은 바로 실행된다.Promise유틸리티는 전달받은 여러Promise의 결과를 기준에 맞게 처리한다.Promise.all()은 모든 작업이 성공해야 성공한다.Promise.all()은 하나라도 실패하면 전체 실패 흐름으로 넘어가지만, 다른 작업을 자동 취소하지는 않는다.Promise.allSettled()는 모든 작업이 끝날 때까지 기다린다.Promise.race()는 가장 먼저 끝난 작업의 결과를 사용한다.Promise.race()는 가장 먼저 끝난 결과를 선택할 뿐, 나머지 작업을 자동 취소하지는 않는다.Promise.any()는 가장 먼저 성공한 작업의 결과를 사용한다.async/await은Promise기반 흐름을 더 읽기 쉽게 작성하는 방식이다.
Promise를 이해할 때는 비동기 작업을 “대기 → 성공 또는 실패 → 결과 처리 → 다음 흐름 연결”로 읽어야 한다.
이 흐름을 잡으면.then(),.catch(),.finally()와 여러Promise유틸리티의 역할도 자연스럽게 이어진다.
async/await은Promise기반 비동기 코드를 더 읽기 쉽게 작성하기 위한 문법이다.
async/await은ES6자체에 포함된 문법은 아니지만,Promise를 실제 코드에서 읽기 쉽게 다룰 때 자주 사용하는 현대JavaScript문법이다.
Promise를 사용하면.then(),.catch()로 성공과 실패를 처리할 수 있다.
하지만 비동기 작업이 여러 단계로 이어지면.then()이 계속 연결되고, 코드 흐름을 따라가기 어려워질 수 있다.
async/await은 이런 비동기 흐름을 위에서 아래로 읽을 수 있게 도와준다.
겉으로는 일반적인 동기 코드처럼 보이지만, 실제로는Promise를 기반으로 동작한다.
async/await은Promise를 없애는 문법이 아니라,Promise를 더 읽기 쉬운 형태로 작성하게 해 주는 문법이다.
그래서async/await을 이해하려면 먼저Promise의 성공, 실패, 대기 흐름을 알고 있어야 한다.
async/await이 등장한 이유
비동기 작업은 한 번만 실행될 때보다 여러 단계가 순서대로 이어질 때 더 복잡해진다.
예전에는 이런 비동기 작업을callback함수로 처리하는 경우가 많았다.
callback함수는 다른 함수에 전달해 두었다가 나중에 실행되는 함수이다.
문제는 비동기 작업이 여러 단계로 이어질 때 생긴다.
첫 번째 작업이 끝난 뒤 두 번째 작업을 실행하고, 두 번째 작업이 끝난 뒤 세 번째 작업을 실행해야 하면callback이 안쪽으로 계속 들어간다.
이렇게 코드가 오른쪽으로 깊어지는 구조를Callback Hell이라고 부른다.
초보자 기준에서는 “작업이 이어질수록 코드가 점점 안쪽으로 파고들어 읽기 어려워지는 상태”라고 이해하면 된다.
Promise는 이 문제를 개선했다.
Promise를 사용하면.then()을 이어 붙여 작업 흐름을 한 줄 방향으로 연결할 수 있다.
그래서callback이 계속 중첩되는 구조보다 읽기 쉬워진다.
하지만.then()이 많아지면 여전히 코드가 길어진다.
또 실패 처리를 위해.catch()흐름을 따로 봐야 하므로, 일반적인 위에서 아래로 읽는 코드와는 모양이 다르다.
이때 등장하는 방식이async/await이다.
async/await은Promise의 기능은 그대로 사용하면서, 코드를 작성하는 사람이 더 읽기 쉽게 만든 문법이다.
이런 문법을 문법적 설탕이라고 한다.
문법적 설탕은 기능 자체를 새로 만드는 것이 아니라, 같은 기능을 더 읽기 쉽고 쓰기 편한 형태로 제공하는 문법을 의미한다.
callback방식은 작업이 이어질수록 코드가 안쪽으로 깊어진다.
Promise는.then()체이닝으로 흐름을 연결한다.
async/await은 같은 비동기 흐름을 위에서 아래로 읽히게 만들어 코드 흐름을 더 직관적으로 보여 준다.
정리하면 흐름은 아래처럼 볼 수 있다.
callback방식은 비동기 작업이 많아질수록 코드가 중첩된다.Promise는.then()체이닝으로 비동기 흐름을 연결한다.async/await은Promise기반 흐름을 위에서 아래로 읽기 쉽게 만든다.
async/await은callback중첩과 긴.then()체이닝의 가독성 문제를 줄이기 위해 사용한다.
async 함수는 항상 Promise를 반환한다
async는 함수를 비동기 함수로 만들 때 사용하는 키워드이다.
함수 앞에async를 붙이면 그 함수는 항상Promise를 반환한다.
여기서 중요한 점은 함수 안에서 일반 값을return해도, 바깥에서는 그 값이Promise로 감싸져서 나온다는 것이다.
즉,async함수의 결과는 바로 값처럼 보이더라도 실제로는Promise흐름으로 처리된다.
아래 이미지는async함수,await키워드, 비동기 흐름의 핵심을 함께 보여 준다.
async함수는 항상Promise를 반환하고,await은Promise가 완료될 때까지 해당 위치에서 기다린다.
다만 기다리는 동안 전체 스레드를 막는 것은 아니며, 다른 코드는 계속 실행될 수 있다.
아래 예제는async함수가 문자열을 반환하는 코드이다.// async-function-return.js async function getMessage() { // async 함수 선언 return "작업 성공"; // 일반 값을 반환 } const result = getMessage(); // async 함수 호출 console.log(result instanceof Promise); // Promise 객체인지 확인 result.then((message) => { // Promise 성공 결과 처리 console.log(message); // 반환된 값 출력 });// 출력결과 // true // 작업 성공
getMessage()안에서는"작업 성공"이라는 문자열을return했다.
하지만getMessage()를 호출한 결과는 문자열 자체가 아니라Promise이다.
그래서result instanceof Promise의 결과가true로 나온다.
이 말은result가 일반 문자열이 아니라Promise객체라는 뜻이다.
그리고result.then()을 사용하면 그Promise의 성공 결과를 받을 수 있다.
async함수는 함수 안에서 어떤 값을 반환하더라도 항상Promise를 반환한다.
이 점을 모르면async함수의 결과를 바로 일반 값처럼 사용하려고 해서 헷갈릴 수 있다.
await은 Promise가 처리될 때까지 해당 위치에서 기다린다
await은Promise의 처리 결과가 나올 때까지 해당async함수 안의 다음 흐름을 잠시 기다리게 하는 문법이다.
Promise가 성공하면 성공 결과를 꺼내고, 실패하면 그 실패가 오류처럼catch로 넘어간다.
await은 보통async함수 안에서 사용한다.
await을 사용하면.then()으로 결과를 받는 대신, 비동기 작업의 성공 결과를 변수에 바로 담는 것처럼 작성할 수 있다.
그래서 코드가 위에서 아래로 순서대로 실행되는 것처럼 읽힌다.
아래 예제는Promise성공 결과를await으로 받는 코드이다.// async-await-basic.js function fetchData() { // 데이터 조회 함수 return Promise.resolve("데이터 조회 성공"); // 성공 Promise 반환 } async function run() { // async 함수 선언 const data = await fetchData(); // Promise 완료 후 성공 결과 받기 console.log(data); // 받은 결과 출력 } run(); // async 함수 실행// 출력결과 // 데이터 조회 성공
fetchData()는 성공한Promise를 반환한다.
await fetchData()는 그Promise가 완료될 때까지 기다린 뒤, 성공 결과인"데이터 조회 성공"을 꺼낸다.
그 결과가data에 들어가고,console.log(data)에서 출력된다.
await은Promise의 성공 결과를 변수에 담아 다음 줄에서 사용할 수 있게 해 준다.
그래서.then()체이닝보다 코드 흐름을 읽기 쉬워진다.
await은 전체 프로그램을 멈추는 것이 아니다
await이라는 단어 때문에 전체 프로그램이 멈춘다고 오해하기 쉽다.
하지만await은 해당async함수 안에서 다음 줄로 넘어가는 것을 기다리게 할 뿐, 전체 실행 환경을 완전히 멈추는 것은 아니다.
아래 예제는await이 있는 함수와 바깥 코드의 실행 순서를 보여 준다.// async-await-order.js function delayMessage() { // 지연된 Promise 반환 함수 return new Promise((resolve) => { // Promise 생성 setTimeout(() => resolve("비동기 작업 완료"), 1000); // 1초 뒤 성공 }); } async function run() { // async 함수 선언 console.log("run 시작"); // 먼저 출력 const message = await delayMessage(); // Promise가 완료될 때까지 run 내부에서 대기 console.log(message); // 완료 후 출력 } run(); // async 함수 실행 console.log("run 바깥 코드"); // run을 기다리지 않고 먼저 실행될 수 있음// 출력결과 // run 시작 // run 바깥 코드 // 비동기 작업 완료
run()이 실행되면 먼저"run 시작"이 출력된다.
그다음await delayMessage()에서run()함수 내부의 흐름은 잠시 기다린다.
하지만 바깥 코드까지 모두 멈추는 것은 아니다.
그래서"run 바깥 코드"가 먼저 출력되고,1초 뒤Promise가 완료되면"비동기 작업 완료"가 출력된다.
await은 해당async함수의 흐름을 기다리게 하는 것이지, 전체 프로그램을 완전히 멈추는 문법은 아니다.
async/await은 then 체이닝을 더 읽기 쉽게 바꾼다
Promise의.then()을 계속 이어 붙이면 작업 흐름이 길어질 수 있다.
이때async/await을 사용하면 같은 흐름을 위에서 아래로 읽는 코드로 바꿀 수 있다.
먼저.then()을 사용한 코드 흐름을 보면 아래와 같다.// promise-then-chain-example.js function getUser() { // 사용자 조회 함수 return Promise.resolve({ id: 1, name: "짱구" }); // 사용자 Promise 반환 } function getPosts(userId) { // 게시글 조회 함수 return Promise.resolve(["게시글1", "게시글2"]); // 게시글 Promise 반환 } getUser() // 사용자 조회 .then((user) => { // 사용자 결과 받기 return getPosts(user.id); // 사용자 id로 게시글 조회 }) .then((posts) => { // 게시글 결과 받기 console.log(posts); // 게시글 출력 });// 출력결과 // ['게시글1', '게시글2']위 코드는 틀린 코드는 아니다.
하지만 작업이 더 많아지면.then()이 계속 이어져서 흐름을 따라가기 어려울 수 있다.
같은 흐름을async/await으로 작성하면 아래처럼 볼 수 있다.// async-await-chain-example.js function getUser() { // 사용자 조회 함수 return Promise.resolve({ id: 1, name: "짱구" }); // 사용자 Promise 반환 } function getPosts(userId) { // 게시글 조회 함수 return Promise.resolve(["게시글1", "게시글2"]); // 게시글 Promise 반환 } async function run() { // async 함수 선언 const user = await getUser(); // 사용자 조회 결과 받기 const posts = await getPosts(user.id); // 사용자 id로 게시글 조회 결과 받기 console.log(posts); // 게시글 출력 } run(); // async 함수 실행// 출력결과 // ['게시글1', '게시글2']이 코드는 먼저
user를 받고, 그user.id를 사용해posts를 받는다.
흐름이 위에서 아래로 이어지기 때문에 동작 순서를 읽기 쉽다.
async/await은 복잡한Promise체이닝을 위에서 아래로 읽히는 코드로 바꿔 가독성을 높인다.
exam26_1과 exam26_2 흐름 비교
앞에서 본
exam26_1.html은Promise와.then(),.finally()를 사용해서 비동기 작업 순서를 연결했다.
이번에는 같은 흐름을async/await으로 바꿔 보면 차이가 더 분명하다.
Promise방식은 작업이 끝난 뒤 실행할 코드를.then()과.finally()에 등록한다.
반면async/await방식은await으로 작업 완료를 기다린 뒤, 다음 줄에 이어서 코드를 작성한다.// exam26_2.html <h2>async/await는 비동기 코드를 순서대로 실행되는 것처럼 보이게 해주는 문법적 설탕이다.</h2> <h3>work()를 await으로 호출하면 Promise 작업이 끝날 때까지 기다리며, resolve() 호출 시 전달되는 값을 결과로 받게 된다.</h3> <h3>await은 async로 선언된 함수 안에서만 사용할 수 있다.</h3> <script> function work() { return new Promise((resolve, reject) => { // Promise 객체 반환 console.log("작업 시작"); // ① 먼저 실행 setTimeout(() => { resolve("작업 완료"); // ② 2초 뒤 성공 결과 전달 }, 2000); }); } async function run() { // async 함수 선언 const result = await work(); // Promise 완료까지 기다린 뒤 결과 받기 console.log(result); // ② 작업 완료 출력 console.log("다음 작업"); // ③ 그 다음 작업 실행 } run(); // async 함수 실행 </script>// 출력결과 // 작업 시작 // 작업 완료 // 다음 작업이 예제에서
run()은async함수이다.
그 안에서await work()를 실행하면work()가 반환한Promise가 완료될 때까지run()내부 흐름이 기다린다.
setTimeout()이 끝나고resolve("작업 완료")가 호출되면 그 값이result에 들어간다.
그다음console.log(result)가 실행되고, 마지막으로"다음 작업"이 출력된다.
exam26_1.html과exam26_2.html은 출력 흐름은 같다.
하지만 코드 모양이 다르다.
Promise방식은.then()과.finally()에 다음 작업을 등록하고,async/await방식은 위에서 아래로 이어지는 문장처럼 작성한다.
async/await은Promise로 만든 비동기 순서 제어를 더 읽기 쉬운 코드 흐름으로 바꿔 준다.
실패 처리는 try/catch로 한다
Promise에서 실패는.catch()로 처리했다.
async/await에서는 실패 처리를 보통try/catch로 작성한다.
try는 오류가 날 수 있는 코드를 시도하는 영역이다.
catch는try안에서 오류가 발생했을 때 그 오류를 받아 처리하는 영역이다.
아래 예제는 실패하는Promise를try/catch로 처리하는 코드이다.// async-await-try-catch.js function fetchData() { // 데이터 조회 함수 return Promise.reject("데이터 조회 실패"); // 실패 Promise 반환 } async function run() { // async 함수 선언 try { // 오류가 날 수 있는 코드 const data = await fetchData(); // 실패하면 catch로 이동 console.log(data); // 실행되지 않음 } catch (error) { // 실패 이유 받기 console.log(error); // 오류 출력 } } run(); // async 함수 실행// 출력결과 // 데이터 조회 실패
fetchData()는 실패한Promise를 반환한다.
그래서await fetchData()에서 실패가 발생하고, 바로catch영역으로 이동한다.
catch의error에는 실패 이유인"데이터 조회 실패"가 들어간다.
그래서"데이터 조회 실패"가 출력된다.
Promise에서.catch()로 실패를 처리했다면,async/await에서는 보통try/catch로 실패를 처리한다고 이해하면 된다.
finally는 성공과 실패에 상관없이 실행된다
try/catch와 함께finally를 사용할 수 있다.
finally는 성공하든 실패하든 마지막에 실행되는 영역이다.
Promise의.finally()가 성공과 실패에 상관없이 실행된 것처럼,try/catch/finally의finally도 마지막 정리 작업에 사용할 수 있다.
아래 예제는 실패하더라도 마지막에"작업 종료"를 출력하는 코드이다.// async-await-finally.js function fetchData() { // 데이터 조회 함수 return Promise.reject("데이터 조회 실패"); // 실패 Promise 반환 } async function run() { // async 함수 선언 try { // 성공 시도 영역 const data = await fetchData(); // 실패하면 catch로 이동 console.log(data); // 실행되지 않음 } catch (error) { // 실패 처리 영역 console.log(error); // 실패 이유 출력 } finally { // 항상 실행되는 영역 console.log("작업 종료"); // 마무리 출력 } } run(); // async 함수 실행// 출력결과 // 데이터 조회 실패 // 작업 종료
fetchData()가 실패했기 때문에catch가 실행된다.
그 뒤finally도 실행되어"작업 종료"가 출력된다.
async/await에서 성공 결과는await으로 받고, 실패 처리는try/catch로 다룬다.
마지막 정리 작업이 필요하면finally를 함께 사용하면 된다.
async/await의 장점
async/await의 가장 큰 장점은 비동기 코드를 읽는 흐름이 좋아진다는 점이다.
await을 사용하면 비동기 작업이 끝난 뒤 다음 작업을 이어서 작성할 수 있으므로, 코드가 위에서 아래로 읽힌다.
두 번째 장점은 오류 처리를try/catch로 통합할 수 있다는 점이다.
Promise체이닝에서는.catch()를 따로 연결해 실패를 처리한다.
async/await에서는 동기 코드에서 쓰던try/catch구조와 비슷하게 실패를 처리할 수 있다.
그래서 성공 흐름과 실패 흐름을 한 함수 안에서 함께 정리하기 쉽다.
세 번째 장점은 디버깅이 비교적 편하다는 점이다.
디버깅은 코드가 어디서 어떤 순서로 실행되는지 확인하면서 문제를 찾는 과정이다.
async/await은 코드가 위에서 아래로 이어지는 형태에 가까워서, 작업 흐름을 한 줄씩 따라가기 쉽다.
정리하면 장점은 아래와 같다.
- 코드가 위에서 아래로 읽혀 가독성이 좋아진다.
- 비동기 실패 처리를
try/catch로 정리할 수 있다.- 코드 실행 흐름을 따라가며 디버깅하기 쉽다.
async/await은 비동기 작업을 동기 코드처럼 보이게 만들어, 읽기 쉬운 흐름과 통합된 오류 처리를 제공한다.
async/await 핵심 정리
async/await은Promise기반 코드를 더 읽기 쉽게 작성하기 위한 문법이다.
비동기 작업 자체가 사라지는 것이 아니라, 비동기 작업의 결과를 다루는 코드 형태가 더 읽기 쉬워진다.
핵심은 아래처럼 정리할 수 있다.
async/await은Promise기반 비동기 코드를 더 읽기 쉽게 작성하는 문법이다.async/await은callback중첩과 긴.then()체이닝의 가독성 문제를 줄이기 위해 사용한다.async/await은 기능을 새로 만드는 문법이라기보다,Promise를 더 읽기 좋게 쓰게 해 주는 문법적 설탕이다.async함수는 항상Promise를 반환한다.async함수 안에서 일반 값을return해도 결과는Promise로 감싸진다.await은Promise의 처리 결과가 나올 때까지 해당async함수 안의 다음 흐름을 기다리게 한다.await은 보통async함수 안에서 사용한다.await은Promise가 성공하면 성공 결과를 변수에 담아 사용할 수 있게 한다.await중인Promise가 실패하면 실패 흐름은try/catch의catch로 넘어간다.await은 전체 프로그램을 멈추는 것이 아니라 해당async함수의 흐름을 기다리게 한다..then()체이닝이 길어질 때async/await을 사용하면 위에서 아래로 읽기 쉬워진다.- 실패 처리는 보통
try/catch로 작성한다.- 성공과 실패에 상관없는 마무리 작업은
finally에서 처리할 수 있다.
async/await을 이해할 때는Promise를 없애는 문법이 아니라,Promise의 성공과 실패 흐름을 더 읽기 쉽게 작성하는 문법이라고 봐야 한다.
Promise의 상태와 결과 처리 흐름을 알고 있으면async/await도 자연스럽게 이어서 이해할 수 있다.
Proxy는 객체를 바로 사용하지 않고, 중간에서 객체 접근을 가로채서 제어할 수 있게 해 주는 문법이다.
여기서 가로챈다는 말은 객체의 값을 읽거나 바꾸는 순간에 중간 코드를 먼저 실행할 수 있다는 뜻이다.
일반 객체는 값을 읽으면 바로 값이 나오고, 값을 바꾸면 바로 바뀐다.
하지만Proxy를 사용하면 값을 읽기 전에 확인하거나, 값을 바꾸기 전에 검사하거나, 없는 값을 요청했을 때 기본값을 돌려주는 식으로 동작을 조절할 수 있다.
Proxy는 원본 객체 앞에 중간 관리자를 세워서, 객체의 읽기와 쓰기 같은 동작을 제어하는 문법이다.
객체 자체를 바꾸는 문법이라기보다, 객체를 다루는 방식을 중간에서 조절하는 문법이라고 이해하면 된다.
Proxy는 원본 객체와 handler로 만든다
Proxy를 만들 때는new Proxy()를 사용한다.
new Proxy()에는 두 가지가 들어간다.
첫 번째는target이다.
target은 실제로 보호하거나 감시할 원본 객체이다.
두 번째는handler이다.
handler는 원본 객체에 어떤 동작이 일어났을 때 중간에서 실행할 규칙을 담은 객체이다.
형태는 아래처럼 볼 수 있다.// proxy-basic-structure.js const target = { // 원본 객체 nickname: "짱구", // nickname 프로퍼티 age: 5 // age 프로퍼티 }; const handler = { // 중간에서 동작을 제어할 규칙 get(target, property) { // 값을 읽을 때 실행 return target[property]; // 원본 객체의 값 반환 } }; const proxy = new Proxy(target, handler); // Proxy 객체 생성 console.log(proxy.nickname); // Proxy를 통해 nickname 읽기 console.log(proxy.age); // Proxy를 통해 age 읽기// 출력결과 // 짱구 // 5이 코드에서
target은 실제 데이터를 가진 원본 객체이다.
handler는 값을 읽을 때 어떻게 처리할지 정하는 규칙 객체이다.
proxy는 원본 객체를 감싼 중간 객체이다.
중요한 점은 값을 읽을 때target.nickname이 아니라proxy.nickname을 사용했다는 것이다.
Proxy의 규칙은proxy를 통해 접근할 때 실행된다.
Proxy를 만들었다면 원본 객체를 직접 사용하는 것이 아니라,Proxy객체를 통해 접근해야 중간 제어 코드가 실행된다.
get은 값을 읽을 때 실행된다
get은 객체의 프로퍼티 값을 읽을 때 실행되는 함수이다.
여기서 프로퍼티는 객체 안에 들어 있는 이름과 값의 묶음이다.
예를 들어{ nickname: "짱구" }에서nickname이 프로퍼티 이름이고,"짱구"가 값이다.
proxy.nickname처럼 값을 읽으면handler안의get()이 실행된다.
그래서 값을 그냥 반환할 수도 있고, 읽기 전에 로그를 남기거나, 없는 값에 기본값을 줄 수도 있다.
아래 예제는 어떤 프로퍼티를 읽는지 확인하는 코드이다.// proxy-get-log.js const target = { // 원본 객체 nickname: "짱구", // nickname 프로퍼티 age: 5 // age 프로퍼티 }; const handler = { get(target, property) { // 값을 읽을 때 실행 console.log(`${property} 값을 읽습니다.`); // 읽는 프로퍼티 이름 출력 return target[property]; // 원본 객체의 값 반환 } }; const proxy = new Proxy(target, handler); // Proxy 객체 생성 console.log(proxy.nickname); // nickname 읽기 console.log(proxy.age); // age 읽기// 출력결과 // nickname 값을 읽습니다. // 짱구 // age 값을 읽습니다. // 5
proxy.nickname을 읽는 순간get()이 실행된다.
이때property에는"nickname"이 들어간다.
그래서"nickname 값을 읽습니다."가 먼저 출력되고, 원본 객체의nickname값인"짱구"가 반환된다.
proxy.age를 읽을 때도 같은 흐름이다.
property에는"age"가 들어가고, 원본 객체의age값인5가 반환된다.
없는 프로퍼티를 읽을 때 기본값을 줄 수 있다
일반 객체에서 존재하지 않는 프로퍼티를 읽으면
undefined가 나온다.
여기서undefined는 값이 정의되어 있지 않다는 뜻이다.
Proxy의get()을 사용하면 없는 프로퍼티를 읽었을 때 원하는 기본값을 돌려줄 수 있다.
아래 예제는 존재하지 않는 값이면"값 없음"을 반환한다.// proxy-get-default.js const target = { // 원본 객체 nickname: "짱구" // nickname 프로퍼티 }; const handler = { get(target, property) { // 값을 읽을 때 실행 if (property in target) { // 원본 객체에 해당 프로퍼티가 있는지 확인 return target[property]; // 있으면 원래 값 반환 } return "값 없음"; // 없으면 기본값 반환 } }; const proxy = new Proxy(target, handler); // Proxy 객체 생성 console.log(proxy.nickname); // 존재하는 값 읽기 console.log(proxy.age); // 존재하지 않는 값 읽기// 출력결과 // 짱구 // 값 없음
proxy.nickname은 원본 객체에 있는 프로퍼티이다.
그래서"짱구"가 반환된다.
하지만proxy.age는 원본 객체에 없다.
일반 객체라면undefined가 나오지만, 이 예제에서는get()에서"값 없음"을 반환하도록 만들었다.
이처럼Proxy를 사용하면 객체를 읽는 동작에 규칙을 추가할 수 있다.
get()은 객체 값을 읽는 순간에 실행되므로, 읽기 로그, 기본값 처리, 접근 제한 같은 작업에 사용할 수 있다.
set은 값을 바꿀 때 실행된다
set은 객체의 프로퍼티 값을 저장하거나 수정할 때 실행되는 함수이다.
즉,proxy.age = 6처럼 값을 넣는 순간handler안의set()이 실행된다.
set()을 사용하면 값을 저장하기 전에 검사할 수 있다.
예를 들어 나이는 숫자여야 하고, 음수가 되면 안 된다는 규칙을 만들 수 있다.
아래 예제는age에 음수를 넣지 못하게 막는 코드이다.// proxy-set-validation.js const target = { // 원본 객체 nickname: "짱구", // nickname 프로퍼티 age: 5 // age 프로퍼티 }; const handler = { set(target, property, value) { // 값을 저장할 때 실행 if (property === "age" && value < 0) { // age에 음수를 넣는지 확인 console.log("나이는 음수가 될 수 없습니다."); // 오류 메시지 출력 return false; // 저장 실패 } target[property] = value; // 조건을 통과하면 값 저장 return true; // 저장 성공 } }; const proxy = new Proxy(target, handler); // Proxy 객체 생성 proxy.age = 6; // 정상 값 저장 console.log(proxy.age); // 변경된 age 출력 proxy.age = -1; // 잘못된 값 저장 시도 console.log(proxy.age); // 기존 age 유지 확인// 출력결과 // 6 // 나이는 음수가 될 수 없습니다. // 6처음에
proxy.age = 6을 실행하면set()이 실행된다.
value는6이고, 음수가 아니므로 원본 객체에 저장된다.
그래서proxy.age를 출력하면6이 나온다.
그다음proxy.age = -1을 실행하면 다시set()이 실행된다.
이번에는value가-1이므로 조건에 걸린다.
그래서"나이는 음수가 될 수 없습니다."가 출력되고, 값은 저장되지 않는다.
set()은 객체 값을 바꾸기 전에 검사하거나, 잘못된 값 저장을 막을 때 사용할 수 있다.
set은 성공 여부를 반환해야 한다
set()은 값을 저장한 뒤 성공 여부를 반환해야 한다.
값 저장이 성공했으면true를 반환하고, 저장을 막았다면false를 반환한다.
초보자 입장에서는return true가 없어도 값만 넣으면 될 것처럼 보일 수 있다.
하지만Proxy의set()은 저장 작업이 성공했는지 알려 주는 구조이다.
특히 엄격 모드에서는set()이false를 반환하면 오류가 발생할 수 있다.
그래서 값을 정상적으로 저장했다면return true를 작성하는 습관이 좋다.// proxy-set-return-true.js const target = { // 원본 객체 count: 0 // count 프로퍼티 }; const handler = { set(target, property, value) { // 값을 저장할 때 실행 target[property] = value; // 원본 객체에 값 저장 return true; // 저장 성공 반환 } }; const proxy = new Proxy(target, handler); // Proxy 객체 생성 proxy.count = 10; // count 값 변경 console.log(proxy.count); // 변경된 count 출력// 출력결과 // 10이 예제에서
proxy.count = 10을 실행하면set()이 실행된다.
target[property] = value로 원본 객체의 값을 바꾼 뒤true를 반환한다.
이렇게 하면Proxy에게 “값 저장이 정상적으로 끝났다”는 신호를 주는 흐름이 된다.
Reflect를 사용하면 기본 동작을 안전하게 호출할 수 있다
get()과set()안에서target[property]처럼 직접 값을 읽고 쓸 수도 있다.
하지만 기본 동작을 그대로 실행하고 싶을 때는Reflect를 사용할 수 있다.
Reflect는 객체의 기본 동작을 함수 형태로 실행할 수 있게 해 주는 도구이다.
예를 들어Reflect.get(target, property)는 원본 객체의 값을 읽는 기본 동작을 실행한다.
Reflect.set(target, property, value)는 원본 객체에 값을 저장하는 기본 동작을 실행한다.
아래 예제는Reflect로 기본 읽기와 쓰기를 처리하는 코드이다.// proxy-reflect-basic.js const target = { // 원본 객체 nickname: "짱구", // nickname 프로퍼티 age: 5 // age 프로퍼티 }; const handler = { get(target, property) { // 값을 읽을 때 실행 console.log(`${property} 읽기`); // 읽기 로그 출력 return Reflect.get(target, property); // 기본 읽기 동작 실행 }, set(target, property, value) { // 값을 저장할 때 실행 console.log(`${property} 변경`); // 변경 로그 출력 return Reflect.set(target, property, value); // 기본 저장 동작 실행 } }; const proxy = new Proxy(target, handler); // Proxy 객체 생성 console.log(proxy.nickname); // nickname 읽기 proxy.age = 6; // age 변경 console.log(proxy.age); // age 읽기// 출력결과 // nickname 읽기 // 짱구 // age 변경 // age 읽기 // 6이 코드에서
Reflect.get()은 원래 객체에서 값을 읽는 기본 동작을 대신 실행한다.
Reflect.set()은 원래 객체에 값을 저장하는 기본 동작을 대신 실행한다.
즉,Proxy로 중간 동작을 추가하되, 실제 읽기와 쓰기는 기본 방식에 맡길 수 있다.
초보자 단계에서는Reflect를 “원래 객체 동작을 안전하게 대신 실행해 주는 도구” 정도로 이해하면 된다.
Proxy를 거치지 않으면 handler가 실행되지 않는다
Proxy에서 가장 중요한 점은proxy를 통해 접근해야handler가 실행된다는 것이다.
원본 객체인target을 직접 사용하면Proxy의get()이나set()은 실행되지 않는다.
아래 예제를 보면 차이가 확실하다.// proxy-target-direct.js const target = { // 원본 객체 nickname: "짱구" // nickname 프로퍼티 }; const handler = { get(target, property) { // Proxy를 통해 값을 읽을 때 실행 console.log("Proxy를 통해 읽었습니다."); // 읽기 로그 출력 return target[property]; // 원본 값 반환 } }; const proxy = new Proxy(target, handler); // Proxy 객체 생성 console.log(proxy.nickname); // Proxy를 통해 읽기 console.log(target.nickname); // 원본 객체를 직접 읽기// 출력결과 // Proxy를 통해 읽었습니다. // 짱구 // 짱구첫 번째
console.log(proxy.nickname)은proxy를 통해 값을 읽는다.
그래서get()이 실행되고"Proxy를 통해 읽었습니다."가 출력된다.
두 번째console.log(target.nickname)은 원본 객체를 직접 읽는다.
이 경우에는Proxy를 거치지 않았으므로get()이 실행되지 않는다.
Proxy의 규칙을 적용하려면 원본 객체가 아니라Proxy객체를 통해 값을 읽거나 바꿔야 한다.
이 점을 놓치면handler를 작성했는데도 아무 동작이 일어나지 않는 것처럼 보일 수 있다.
Proxy 핵심 정리
Proxy는 원본 객체 앞에서 객체의 기본 동작을 가로채는 중간 객체이다.
객체 값을 읽거나 바꾸는 순간에 원하는 코드를 먼저 실행할 수 있다.
핵심은 아래처럼 정리할 수 있다.
Proxy는 객체 접근을 중간에서 가로채서 제어하는 문법이다.target은 실제 데이터를 가진 원본 객체이다.handler는 중간에서 실행할 규칙을 담은 객체이다.new Proxy(target, handler)로Proxy객체를 만든다.get()은 프로퍼티 값을 읽을 때 실행된다.set()은 프로퍼티 값을 저장하거나 수정할 때 실행된다.get()을 사용하면 읽기 로그를 남기거나 없는 값에 기본값을 줄 수 있다.set()을 사용하면 값을 저장하기 전에 검사할 수 있다.set()은 저장 성공 여부를true또는false로 반환해야 한다.Reflect.get()과Reflect.set()은 원래 객체의 기본 읽기와 쓰기 동작을 실행한다.- 원본 객체를 직접 사용하면
handler가 실행되지 않는다.Proxy의 규칙을 적용하려면Proxy객체를 통해 접근해야 한다.
Proxy의 핵심은 원본 객체를 직접 다루지 않고, 중간 객체를 통해 읽기와 쓰기 동작을 감시하거나 제어할 수 있다는 점이다.
이 구조를 이해하면 값 검증, 기본값 처리, 접근 로그처럼 객체 사용 흐름을 더 세밀하게 관리할 수 있다.
JSON은 데이터를 문자열 형태로 표현하기 위한 형식이다.
여기서 문자열 형태라는 말은 사람이 보기에는 객체나 배열처럼 보일 수 있지만, 실제로는 글자 데이터로 주고받는다는 뜻이다.
JavaScript객체는 코드 안에서 바로 사용할 수 있는 데이터 구조이다.
반면JSON은 서버와 클라이언트가 데이터를 주고받거나, 데이터를 저장할 때 자주 사용하는 텍스트 형식이다.
예를 들어 사용자 정보를 서버로 보낸다고 생각해 보자.
JavaScript객체 그대로 보내는 것이 아니라, 보통 문자열 형태의JSON으로 바꿔서 전송한다.
반대로 서버에서 받은JSON문자열은 다시JavaScript객체나 배열로 바꿔야 코드에서 사용할 수 있다.
JSON은 객체 자체가 아니라, 데이터를 주고받기 위해 정해진 문자열 형식이다.
이 차이를 이해해야JSON.stringify()와JSON.parse()가 왜 필요한지 알 수 있다.
JSON은 데이터를 표현하는 문자열 형식이다
JSON은JavaScript Object Notation의 줄임말이다.
뜻을 풀면JavaScript객체 표기법이라는 의미이다.
이름에JavaScript가 들어가지만, 실제로는 여러 환경에서 데이터를 주고받을 때 사용하는 공통 데이터 형식으로 많이 쓰인다.
JSON은 객체나 배열과 비슷하게 생겼지만, 코드에서 바로 사용하는 객체나 배열과는 다르다.
JSON은 문자열이다.
즉, 데이터를 글자 형태로 표현한 것이다.
아래 예제는 사용자 정보를JSON문자열로 표현한 모습이다.// json-string-basic.js const jsonText = '{"nickname":"짱구","age":5,"city":"떡잎마을"}'; // JSON 문자열 console.log(jsonText); // JSON 문자열 출력 console.log(typeof jsonText); // 자료형 확인// 출력결과 // {"nickname":"짱구","age":5,"city":"떡잎마을"} // string
jsonText에 들어 있는 값은 객체처럼 보인다.
하지만 양쪽에 따옴표가 있는 문자열이다.
그래서typeof jsonText의 결과는object가 아니라string이다.
이 상태에서는jsonText.nickname처럼 객체 프로퍼티에 접근할 수 없다.
아직JSON문자열일 뿐이기 때문이다.
JSON은 객체나 배열처럼 생길 수 있는 문자열이고, 실제 객체나 배열로 사용하려면 변환 과정이 필요하다.
JavaScript 객체와 JSON은 비슷하지만 다르다
JavaScript객체와JSON은 모양이 비슷하다.
그래서 처음에는 같은 것처럼 보일 수 있다.
하지만 문법 규칙이 다르다.
JavaScript객체는 코드 안에서 사용하는 실제 데이터이다.
객체의key에 따옴표를 붙이지 않아도 되고, 문자열에 작은따옴표를 사용할 수도 있다.
여기서key는 객체에서 값을 구분하는 이름이다.
아래 예제는JavaScript객체이다.// javascript-object-basic.js const user = { // JavaScript 객체 nickname: "짱구", // key에 따옴표를 쓰지 않아도 됨 age: 5, // 숫자 값 city: "떡잎마을" // 문자열 값 }; console.log(user.nickname); // 객체 프로퍼티 접근 console.log(typeof user); // 자료형 확인// 출력결과 // 짱구 // object이 코드는 실제
JavaScript객체이다.
그래서user.nickname처럼 점 표기법으로 값을 꺼낼 수 있다.
typeof user의 결과도object이다.
반면JSON에서는 규칙이 더 엄격하다.
객체의key는 반드시 큰따옴표로 감싸야 한다.
문자열 값도 큰따옴표로 감싸야 한다.
작은따옴표를 사용할 수 없고, 주석도 넣을 수 없다.
아래 예제는 올바른JSON문자열이다.// json-valid-string.js const jsonText = '{"nickname":"짱구","age":5,"city":"떡잎마을"}'; // 올바른 JSON 문자열 console.log(jsonText); // JSON 문자열 출력// 출력결과 // {"nickname":"짱구","age":5,"city":"떡잎마을"}차이를 정리하면 아래처럼 볼 수 있다.
JavaScript객체는 코드 안에서 바로 사용하는 실제 객체이다.JSON은 데이터를 표현한 문자열 형식이다.JSON의key는 반드시 큰따옴표로 감싸야 한다.JSON의 문자열 값도 반드시 큰따옴표로 감싸야 한다.JSON에는 함수,undefined, 주석을 넣을 수 없다.
JSON은JavaScript객체와 비슷하게 생겼지만, 더 엄격한 문자열 데이터 형식이다.
JSON.stringify는 JavaScript 값을 JSON 문자열로 바꾼다
JSON.stringify()는 객체나 배열 같은JavaScript값을JSON문자열로 바꾸는 메서드이다.
여기서 메서드는 객체가 가지고 있는 함수라고 이해하면 된다.
서버로 데이터를 보내거나, 문자열로 저장해야 할 때는 객체나 배열 같은 값을 그대로 사용할 수 없는 경우가 많다.
이때JSON.stringify()를 사용해서JavaScript값을 문자열로 바꾼다.
아래 예제는JavaScript객체를JSON문자열로 바꾸는 코드이다.// json-stringify-basic.js const user = { // JavaScript 객체 nickname: "짱구", // 닉네임 age: 5, // 나이 city: "떡잎마을" // 도시 }; const jsonText = JSON.stringify(user); // 객체를 JSON 문자열로 변환 console.log(jsonText); // 변환된 JSON 문자열 출력 console.log(typeof jsonText); // 자료형 확인// 출력결과 // {"nickname":"짱구","age":5,"city":"떡잎마을"} // string
user는 처음에JavaScript객체이다.
그래서 코드 안에서user.nickname처럼 사용할 수 있다.
JSON.stringify(user)를 실행하면 객체가JSON문자열로 바뀐다.
그 결과가jsonText에 저장된다.
그래서typeof jsonText의 결과는string이다.
JSON.stringify()는 코드에서 쓰는 객체나 배열 같은JavaScript값을 전송하거나 저장하기 쉬운 문자열 형태로 바꾼다.
JSON.parse는 JSON 문자열을 JavaScript 값으로 바꾼다
JSON.parse()는JSON문자열을 코드에서 사용할 수 있는JavaScript값으로 바꾸는 메서드이다.
대부분 객체나 배열 형태의JSON문자열을 객체나 배열로 바꿀 때 사용한다.
서버에서 받은 데이터가JSON문자열이라면, 코드에서 사용하기 위해 객체나 배열로 바꿔야 한다.
문자열 상태에서는 점 표기법이나 배열 인덱스로 원하는 값을 제대로 다루기 어렵기 때문이다.
아래 예제는JSON문자열을 객체로 바꾸는 코드이다.// json-parse-basic.js const jsonText = '{"nickname":"짱구","age":5,"city":"떡잎마을"}'; // JSON 문자열 const user = JSON.parse(jsonText); // JSON 문자열을 JavaScript 객체로 변환 console.log(user.nickname); // 객체 프로퍼티 접근 console.log(user.age); // 객체 프로퍼티 접근 console.log(typeof user); // 자료형 확인// 출력결과 // 짱구 // 5 // object
jsonText는 처음에는 문자열이다.
하지만JSON.parse(jsonText)를 실행하면JavaScript객체로 변환된다.
그 결과를user에 저장했기 때문에user.nickname,user.age처럼 객체의 값을 꺼낼 수 있다.
typeof user의 결과도object가 된다.
JSON.parse()는 문자열로 받은JSON데이터를 코드에서 사용할 수 있는 객체나 배열 같은JavaScript값으로 바꾼다.
JSON 문자열이 올바르지 않으면 parse 오류가 난다
JSON.parse()는 아무 문자열이나JavaScript값으로 바꿔 주지 않는다.
문자열이 올바른JSON형식이어야 한다.
특히JSON에서는key와 문자열 값을 큰따옴표로 감싸야 한다.
작은따옴표를 사용하거나,key에 따옴표를 붙이지 않으면 오류가 날 수 있다.
아래 코드는 잘못된 예시이다.// json-parse-error.js const wrongJsonText = "{nickname:'짱구',age:5}"; // 잘못된 JSON 문자열 const user = JSON.parse(wrongJsonText); // JSON 형식이 아니므로 오류 발생 console.log(user); // 실행되지 않음// 출력결과 // SyntaxError이 코드가 오류 나는 이유는
wrongJsonText가 올바른JSON형식이 아니기 때문이다.
nickname이라는key에 큰따옴표가 없고,"짱구"문자열도 작은따옴표로 작성되어 있다.
올바른JSON문자열은 아래처럼 작성해야 한다.// json-parse-correct.js const correctJsonText = '{"nickname":"짱구","age":5}'; // 올바른 JSON 문자열 const user = JSON.parse(correctJsonText); // JSON 문자열을 객체로 변환 console.log(user.nickname); // 변환된 객체 값 출력// 출력결과 // 짱구
JSON.parse()를 사용할 때는 문자열이 반드시 올바른JSON형식인지 확인해야 한다.
형식이 맞지 않으면 객체나 배열 같은JavaScript값으로 변환되지 않고 오류가 발생한다.
JSON에서 사용할 수 있는 값은 정해져 있다
JSON에는 아무 값이나 넣을 수 있는 것이 아니다.
JSON에서 사용할 수 있는 값은 정해져 있다.
JSON에서 사용할 수 있는 대표적인 값은 문자열, 숫자, 참과 거짓, 배열, 객체,null이다.
여기서null은 값이 없음을 의도적으로 나타내는 값이다.
반대로undefined, 함수, 주석은JSON값으로 사용할 수 없다.
undefined는 값이 정의되지 않았다는 뜻이고, 함수는 실행 가능한 코드이다.
JSON은 데이터를 표현하는 형식이므로 실행 코드인 함수는 들어갈 수 없다.
아래 예제는JSON.stringify()를 사용할 때undefined와 함수가 어떻게 처리되는지 보여 준다.// json-stringify-unsupported.js const user = { // JavaScript 객체 nickname: "짱구", // 문자열 값 age: 5, // 숫자 값 hobby: undefined, // JSON에서 표현할 수 없는 값 sayHello: function () { // JSON에서 표현할 수 없는 함수 return "안녕"; } }; const jsonText = JSON.stringify(user); // 객체를 JSON 문자열로 변환 console.log(jsonText); // 변환 결과 출력// 출력결과 // {"nickname":"짱구","age":5}
JSON.stringify()는 객체를JSON문자열로 바꿀 때undefined와 함수 값을 제외한다.
그래서 결과에는nickname과age만 남는다.
이 예제에서 중요한 점은JSON이 코드 전체를 저장하는 형식이 아니라는 것이다.
JSON은 실행할 함수나 정의되지 않은 값이 아니라, 주고받을 데이터만 표현하는 형식이다.
정리하면 아래처럼 볼 수 있다.
JSON에서 문자열은 큰따옴표로 감싼다.JSON에서 숫자는 그대로 작성할 수 있다.JSON에서 참과 거짓은true,false로 표현한다.JSON에서 값이 없음을 나타낼 때는null을 사용할 수 있다.JSON에는undefined, 함수, 주석을 넣을 수 없다.
JSON은 데이터를 주고받기 위한 형식이므로, 실행 코드나 정의되지 않은 값은 표현 대상이 아니다.
객체와 배열 모두 JSON 문자열로 바꿀 수 있다
JSON.stringify()는 객체뿐 아니라 배열에도 사용할 수 있다.
배열은 여러 값을 순서대로 담는 구조이다.
배열도 서버로 보내거나 저장할 때 문자열로 바꿔야 할 수 있다.
아래 예제는 객체 배열을JSON문자열로 바꾸고 다시 객체 배열로 되돌리는 코드이다.// json-array-stringify-parse.js const users = [ // 객체가 들어 있는 배열 { nickname: "짱구", age: 5 }, // 첫 번째 사용자 { nickname: "철수", age: 5 } // 두 번째 사용자 ]; const jsonText = JSON.stringify(users); // 배열을 JSON 문자열로 변환 const parsedUsers = JSON.parse(jsonText); // JSON 문자열을 다시 배열로 변환 console.log(jsonText); // JSON 문자열 출력 console.log(parsedUsers[0].nickname); // 변환된 배열의 첫 번째 객체 값 출력// 출력결과 // [{"nickname":"짱구","age":5},{"nickname":"철수","age":5}] // 짱구
users는 객체가 들어 있는 배열이다.
JSON.stringify(users)를 사용하면 배열 전체가JSON문자열로 바뀐다.
그 문자열을JSON.parse(jsonText)로 다시 변환하면 배열 구조를 되찾을 수 있다.
그래서parsedUsers[0].nickname처럼 첫 번째 객체의 값을 꺼낼 수 있다.
이 흐름은 데이터를 저장했다가 다시 꺼내 쓰거나, 서버에서 받은 배열 데이터를 화면에 보여 줄 때 자주 사용된다.
JSON 변환 흐름은 JavaScript 값과 문자열 사이를 오간다
JSON을 사용할 때 가장 중요한 흐름은 두 가지이다.
객체나 배열 같은JavaScript값을 보낼 때는 문자열로 바꾸고, 문자열을 받았을 때는 다시 코드에서 사용할 수 있는 값으로 바꾼다.
JSON.stringify()는 객체나 배열 같은JavaScript값을JSON문자열로 바꾼다.
JSON.parse()는JSON문자열을 객체나 배열 같은JavaScript값으로 바꾼다.
아래 예제로 전체 흐름을 한 번에 볼 수 있다.// json-convert-flow.js const user = { // JavaScript 객체 nickname: "짱구", // 닉네임 age: 5 // 나이 }; const jsonText = JSON.stringify(user); // 객체를 JSON 문자열로 변환 const parsedUser = JSON.parse(jsonText); // JSON 문자열을 다시 객체로 변환 console.log(typeof user); // 원래 객체의 자료형 확인 console.log(typeof jsonText); // JSON 문자열의 자료형 확인 console.log(typeof parsedUser); // 다시 변환한 객체의 자료형 확인 console.log(parsedUser.nickname); // 변환된 객체 값 확인// 출력결과 // object // string // object // 짱구처음
user는JavaScript객체이다.
JSON.stringify(user)를 실행하면jsonText는 문자열이 된다.
다시JSON.parse(jsonText)를 실행하면parsedUser는 객체가 된다.
즉,JSON변환 흐름은 아래처럼 정리할 수 있다.
- 객체 또는 배열 같은
JavaScript값 →JSON.stringify()→JSON문자열JSON문자열 →JSON.parse()→ 객체 또는 배열 같은JavaScript값데이터를 보낼 때는
JSON.stringify(), 데이터를 받아서 코드에서 사용할 때는JSON.parse()를 사용한다.
JSON 핵심 정리
JSON은 데이터를 주고받거나 저장하기 위한 문자열 형식이다.
객체나 배열처럼 생겼지만 실제로는 문자열이므로, 코드에서 객체나 배열처럼 사용하려면 변환이 필요하다.
핵심은 아래처럼 정리할 수 있다.
JSON은 데이터를 표현하는 문자열 형식이다.JSON은JavaScript객체나 배열과 비슷하게 생길 수 있지만 실제 객체나 배열은 아니다.JavaScript객체와 배열은 코드 안에서 바로 사용할 수 있다.JSON문자열은JSON.parse()로 객체나 배열 같은JavaScript값으로 바꿔야 사용할 수 있다.JSON.stringify()는 객체나 배열 같은JavaScript값을JSON문자열로 바꾼다.JSON.parse()는JSON문자열을 객체나 배열 같은JavaScript값으로 바꾼다.JSON의key는 반드시 큰따옴표로 감싸야 한다.JSON의 문자열 값도 반드시 큰따옴표로 감싸야 한다.JSON에는undefined, 함수, 주석을 넣을 수 없다.- 올바르지 않은
JSON문자열을JSON.parse()로 변환하면 오류가 발생한다.
JSON의 핵심은 데이터를 문자열로 바꿔 주고받은 뒤, 다시 객체나 배열 같은JavaScript값으로 바꿔 코드에서 사용하는 흐름이다.
JSON.stringify()와JSON.parse()의 방향을 구분하면JSON을 훨씬 쉽게 이해할 수 있다.
Module은 코드를 기능 단위로 나누어 관리하는 방식이다.
하나의 파일에 모든 코드를 몰아서 작성하면 처음에는 편해 보이지만, 코드가 길어질수록 어디에 어떤 기능이 있는지 찾기 어려워진다.
예를 들어 사용자 정보 처리, 계산 기능, 화면 출력 기능을 한 파일에 모두 넣으면 파일이 점점 복잡해진다.
이때 기능별로 파일을 나누고, 필요한 기능만 가져와서 사용하면 코드 흐름이 훨씬 정리된다.
다만 파일을 나누었다고 해서 모든 파일이 자동으로Module이 되는 것은 아니다.
JavaScript에서Module은 보통import와export를 사용해서 다른 파일과 값을 주고받는 파일을 말한다.
브라우저에서는script type="module"로 실행되는 파일이Module로 동작한다.
Module에서 다른 파일로 값을 내보내는 문법이export이고, 다른 파일에서 값을 가져오는 문법이import이다.
Module의 핵심은 코드를 파일 단위로 나누고, 필요한 값이나 함수만export와import로 연결해서 사용하는 것이다.
이 구조를 이해하면 큰 코드도 기능별로 나누어 관리할 수 있다.
Module이 필요한 이유
Module이 필요한 가장 큰 이유는 코드를 나누어 관리하기 위해서이다.
코드가 짧을 때는 한 파일에 작성해도 크게 문제가 없다.
하지만 기능이 많아지면 한 파일 안에서 변수, 함수, 실행 코드가 뒤섞인다.
이렇게 되면 수정할 때도 불편하다.
어떤 함수가 어디에서 사용되는지 찾기 어렵고, 같은 이름의 변수나 함수가 충돌할 가능성도 커진다.
Module을 사용하면 기능별로 파일을 나눌 수 있다.
예를 들어 계산 기능은 계산 파일에 두고, 화면 출력 기능은 화면 파일에 둘 수 있다.
그리고 필요한 파일에서 필요한 기능만 가져와 사용할 수 있다.
정리하면Module을 사용하는 이유는 아래와 같다.
- 기능별로 코드를 나누어 관리할 수 있다.
- 필요한 코드만 가져와 사용할 수 있다.
- 파일의 역할이 분명해진다.
- 변수나 함수 이름 충돌을 줄일 수 있다.
- 코드 재사용이 쉬워진다.
Module은 코드를 작게 나누고 다시 필요한 곳에서 연결하기 위한 구조이다.
파일을 나누는 것 자체가 목적이 아니라, 역할이 분명한 코드 구조를 만들기 위해 사용한다.
export는 다른 파일에서 사용할 수 있게 내보내는 문법이다
export는 현재 파일 안에 있는 변수나 함수를 다른 파일에서 사용할 수 있게 내보내는 문법이다.
파일 안에 변수나 함수를 만들었다고 해서 다른 파일에서 자동으로 사용할 수 있는 것은 아니다.
다른 파일에서 사용하려면 반드시export로 내보내야 한다.
export하지 않은 값은 그 파일 내부에서만 사용할 수 있다고 이해하면 된다.
아래 예제는 사용자 이름과 인사 함수를 내보내는 코드이다.// user-module.js export const nickname = "짱구"; // 다른 파일에서 사용할 수 있게 변수 내보내기 export function sayHello() { // 다른 파일에서 사용할 수 있게 함수 내보내기 return "안녕하세요"; // 인사 문장 반환 }이 코드에서
nickname과sayHello()앞에export가 붙어 있다.
그래서 다른 파일에서 이 두 값을 가져와 사용할 수 있다.
여기서 중요한 점은export가 값을 실행하는 문법이 아니라는 것이다.
export는 “이 값을 다른 파일에서 가져갈 수 있게 공개하겠다”는 의미이다.
export는 현재 파일의 값이나 함수를 외부 파일에서 사용할 수 있도록 공개하는 문법이다.
import는 다른 파일에서 내보낸 값을 가져오는 문법이다
import는 다른 파일에서export한 값을 현재 파일로 가져오는 문법이다.
가져온 값은 현재 파일 안에서 변수나 함수처럼 사용할 수 있다.
아래 예제는 앞에서 만든user-module.js의 값을 가져와 사용하는 코드이다.// main-import-basic.js import { nickname, sayHello } from "./user-module.js"; // 다른 파일에서 내보낸 값 가져오기 console.log(nickname); // 가져온 변수 출력 console.log(sayHello()); // 가져온 함수 실행 결과 출력// 출력결과 // 짱구 // 안녕하세요
import { nickname, sayHello } from "./user-module.js"는user-module.js파일에서nickname과sayHello를 가져오겠다는 뜻이다.
중괄호 안에는 가져올 이름을 적는다.
이때 이름은export한 이름과 같아야 한다.
그래서nickname으로 내보냈다면 가져올 때도nickname이라고 적어야 한다.
from "./user-module.js"는 어느 파일에서 가져올지 경로를 적는 부분이다.
여기서./는 현재 파일과 같은 위치를 기준으로 찾겠다는 뜻이다.
import는 다른 파일에서export한 값을 현재 파일로 가져와 사용하는 문법이다.
export와import는 따로 떨어진 문법이 아니라, 내보내기와 가져오기로 서로 연결된다.
named export는 이름을 기준으로 내보내고 가져온다
named export는 이름이 있는 값을 내보내는 방식이다.
여기서named는 “이름이 붙은”이라는 뜻이다.
즉, 변수 이름이나 함수 이름을 기준으로 내보내고 가져온다.
앞에서 본export const nickname,export function sayHello()가named export이다.
이 방식에서는 내보낸 이름과 가져오는 이름이 같아야 한다.
아래 예제는 여러 값을named export로 내보내는 코드이다.// math-module.js export const add = (a, b) => { // 더하기 함수 내보내기 return a + b; // 두 수의 합 반환 }; export const subtract = (a, b) => { // 빼기 함수 내보내기 return a - b; // 두 수의 차 반환 };아래 코드는
math-module.js에서 내보낸 두 함수를 가져와 사용한다.// main-named-import.js import { add, subtract } from "./math-module.js"; // 이름을 기준으로 함수 가져오기 console.log(add(10, 5)); // 더하기 결과 출력 console.log(subtract(10, 5)); // 빼기 결과 출력// 출력결과 // 15 // 5
add와subtract는 이름이 있는 상태로 내보내졌다.
그래서 가져올 때도 중괄호 안에{ add, subtract }처럼 정확한 이름을 적는다.
만약 이름을 잘못 적으면 해당 값을 찾을 수 없다.
named export는 이름이 기준이기 때문이다.
정리하면 아래처럼 볼 수 있다.
named export는 여러 값을 내보낼 수 있다.- 가져올 때 중괄호
{}를 사용한다.- 내보낸 이름과 가져오는 이름이 같아야 한다.
named export는 이름을 기준으로 연결되므로,export한 이름과import하는 이름을 맞춰야 한다.
as를 사용하면 가져오는 이름을 바꿀 수 있다
named export는 원래 내보낸 이름과 같은 이름으로 가져와야 한다.
하지만 현재 파일에서 다른 이름으로 사용하고 싶을 때가 있다.
이때as를 사용한다.
as는 가져온 값에 다른 이름을 붙이는 문법이다.
아래 예제는add함수를 가져오면서plus라는 이름으로 바꾸는 코드이다.// main-import-alias.js import { add as plus } from "./math-module.js"; // add를 plus라는 이름으로 가져오기 console.log(plus(3, 4)); // plus 이름으로 함수 실행// 출력결과 // 7
import { add as plus }는math-module.js에서add라는 이름으로 내보낸 값을 가져오되, 현재 파일에서는plus라는 이름으로 사용하겠다는 뜻이다.
이때 실제로 현재 파일에서 사용할 이름은plus이다.
그래서add(3, 4)가 아니라plus(3, 4)로 호출한다.
as는export된 이름을 현재 파일에서 다른 이름으로 사용하고 싶을 때 사용한다.
이름 충돌을 피하거나, 현재 파일의 의미에 맞게 이름을 바꾸고 싶을 때 유용하다.
default export는 파일에서 기본으로 내보내는 값이다
default export는 파일에서 기본값처럼 하나를 내보내는 방식이다.
default는 “기본”이라는 뜻이다.
named export는 이름을 기준으로 여러 값을 내보낼 수 있다.
반면default export는 한 파일에서 하나만 사용할 수 있다.
파일의 대표 기능이나 대표 값을 내보낼 때 주로 사용한다.
아래 예제는 사용자 객체를default export로 내보내는 코드이다.// user-default-module.js const user = { // 사용자 객체 생성 nickname: "짱구", // 닉네임 age: 5 // 나이 }; export default user; // 파일의 기본값으로 user 내보내기이 값을 가져올 때는 중괄호를 사용하지 않는다.
// main-default-import.js import userInfo from "./user-default-module.js"; // default export 값 가져오기 console.log(userInfo.nickname); // 가져온 객체의 nickname 출력 console.log(userInfo.age); // 가져온 객체의 age 출력// 출력결과 // 짱구 // 5
default export는 가져올 때 이름을 자유롭게 정할 수 있다.
위 코드에서는user라는 이름으로 내보냈지만, 가져올 때는userInfo라는 이름으로 받았다.
이것이 가능한 이유는default export가 이름 기준이 아니라, 그 파일의 기본 내보내기 값을 가져오는 방식이기 때문이다.
default export는 한 파일에서 하나만 사용할 수 있고, 가져올 때 중괄호 없이 원하는 이름으로 받을 수 있다.
named export와 default export는 가져오는 방식이 다르다
named export와default export는 모두 다른 파일에서 사용할 값을 내보내는 문법이다.
하지만 가져오는 방식이 다르다.
named export는 이름을 기준으로 가져온다.
그래서 중괄호를 사용하고, 내보낸 이름과 같은 이름을 적어야 한다.
default export는 파일의 기본 내보내기 값을 가져온다.
그래서 중괄호를 사용하지 않고, 가져오는 쪽에서 원하는 이름을 붙일 수 있다.
아래 예제는 두 방식을 한 번에 비교하는 코드이다.// module-mixed-export.js export const version = "1.0.0"; // named export const serviceName = "회원 서비스"; // 기본으로 내보낼 값 export default serviceName; // default export아래 코드는
default export와named export를 함께 가져온다.// main-mixed-import.js import service, { version } from "./module-mixed-export.js"; // default와 named를 함께 가져오기 console.log(service); // default export 값 출력 console.log(version); // named export 값 출력// 출력결과 // 회원 서비스 // 1.0.0
service는default export로 가져온 값이다.
그래서 중괄호 없이 작성했고, 이름도 가져오는 쪽에서 정했다.
version은named export로 가져온 값이다.
그래서 중괄호 안에{ version }처럼 작성했고, 내보낸 이름과 맞춰야 한다.
정리하면 아래처럼 볼 수 있다.
named export는 중괄호를 사용한다.named export는 내보낸 이름과 가져오는 이름이 같아야 한다.default export는 중괄호를 사용하지 않는다.default export는 가져오는 쪽에서 이름을 정할 수 있다.- 한 파일에서
default export는 하나만 사용할 수 있다.
named export와default export를 구분할 때는 중괄호 사용 여부와 이름 기준 여부를 보면 된다.
import * as를 사용하면 모듈 전체를 객체처럼 가져올 수 있다
여러 값을 한 번에 가져오고 싶을 때
import * as를 사용할 수 있다.
이 방식은 해당 파일에서 내보낸 값들을 하나의 객체처럼 묶어서 가져온다.
아래 예제는math-module.js의 모든named export를math라는 이름으로 가져오는 코드이다.// main-import-all.js import * as math from "./math-module.js"; // math-module의 named export 전체 가져오기 console.log(math.add(2, 3)); // math 객체를 통해 add 사용 console.log(math.subtract(10, 4)); // math 객체를 통해 subtract 사용// 출력결과 // 5 // 6
import * as math는math-module.js에서 내보낸 이름 있는 값들을math라는 객체처럼 묶어 가져온다.
그래서math.add(),math.subtract()처럼 접근한다.
이 방식은 가져올 값이 많거나, 어떤 모듈에서 온 기능인지 드러내고 싶을 때 사용할 수 있다.
다만 모든 값을 무조건 이렇게 가져오는 것이 좋은 것은 아니다.
필요한 값이 분명하면 필요한 것만 가져오는 편이 코드 의도를 더 명확하게 보여 줄 수 있다.
브라우저에서 Module을 사용하려면 type="module"이 필요하다
브라우저에서
import와export를 사용하는 파일을 실행하려면script태그에type="module"을 붙여야 한다.
일반script로 실행하면import문법을 사용할 수 없다.
브라우저에서는type="module"이 붙은script파일이 시작점이 된다.
그 시작점 파일 안에서import한 다른 파일들도Module로 연결되어 실행된다.
그래서main-import-basic.js를type="module"로 연결하면, 그 안에서 가져오는user-module.js도Module방식으로 해석된다.
아래 예제는 브라우저에서Module파일을 연결하는 코드이다.<!-- module-index.html --> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <!-- 문자 인코딩 설정 --> <title>Module Example</title> <!-- 문서 제목 --> </head> <body> <h1>Module 예제</h1> <!-- 화면 제목 --> <script type="module" src="./main-import-basic.js"></script> <!-- Module 시작 파일 연결 --> </body> </html>
type="module"을 붙이면 브라우저가 해당script를Module로 해석한다.
그래서main-import-basic.js안에서import문법을 사용할 수 있다.
또Module에서는 파일 경로를 명확히 적어야 한다.
현재 위치의 파일을 가져올 때는 보통./파일명.js처럼 작성한다.
브라우저에서import와export를 사용하려면 시작 파일을script type="module"로 실행해야 한다.
이 설정이 없으면 일반script로 해석되기 때문에Module문법이 정상적으로 동작하지 않는다.
Module은 자체 scope를 가진다
Module은 자체scope를 가진다.
여기서scope는 변수나 함수가 유효한 범위를 말한다.
일반적으로Module파일 안에서 만든 변수나 함수는 다른 파일에서 자동으로 보이지 않는다.
다른 파일에서 사용하려면export로 내보내고, 사용하는 쪽에서import로 가져와야 한다.
아래 예제에서secret은export하지 않았기 때문에 다른 파일에서 직접 가져올 수 없다.// scope-module.js const secret = "비공개 값"; // export하지 않은 값 export const publicValue = "공개 값"; // export한 값아래 코드에서는
publicValue만 가져올 수 있다.// main-scope-import.js import { publicValue } from "./scope-module.js"; // export된 값만 가져오기 console.log(publicValue); // 공개 값 출력// 출력결과 // 공개 값
secret은 같은 파일 안에서는 사용할 수 있지만,export하지 않았으므로 다른 파일에서 가져올 수 없다.
반면publicValue는export했기 때문에import로 가져올 수 있다.
Module파일 안의 값은 기본적으로 그 파일 안에서만 유효하고, 외부에서 사용하려면 반드시export해야 한다.
이 구조 덕분에 불필요한 전역 변수 충돌을 줄일 수 있다.
Module 핵심 정리
Module은 코드를 기능 단위로 나누고, 필요한 값이나 함수만 다른 파일에서 사용할 수 있게 연결하는 구조이다.
큰 코드를 여러 파일로 나누면 역할이 분명해지고, 유지보수가 쉬워진다.
핵심은 아래처럼 정리할 수 있다.
Module은 코드를 파일 단위로 나누어 관리하는 방식이다.- 파일을 나누었다고 모두
Module이 되는 것은 아니며,import/export를 사용하거나type="module"로 실행되는 파일이Module로 동작한다.export는 현재 파일의 값이나 함수를 다른 파일에서 사용할 수 있게 내보낸다.import는 다른 파일에서export한 값을 현재 파일로 가져온다.named export는 이름을 기준으로 내보내고 가져온다.named export는 가져올 때 중괄호{}를 사용한다.as를 사용하면 가져온 값의 이름을 현재 파일에서 바꿔 사용할 수 있다.default export는 한 파일에서 하나만 사용할 수 있다.default export는 가져올 때 중괄호를 사용하지 않는다.import * as는 여러named export를 하나의 객체처럼 묶어서 가져온다.- 브라우저에서
Module을 사용하려면 시작 파일에script type="module"이 필요하다.Module은 자체scope를 가지므로,export하지 않은 값은 다른 파일에서 사용할 수 없다.
Module을 이해할 때는 파일을 나누는 것보다, 어떤 값을 외부에 공개하고 어떤 값을 내부에 감출지 구분하는 것이 중요하다.
export와import의 방향을 구분하면 여러 파일로 나누어진 코드도 자연스럽게 읽을 수 있다.
Symbol은ES6에서 추가된 원시 자료형이다.
원시 자료형은 더 이상 작은 단위로 나눌 수 없는 기본 값이라고 이해하면 된다.
예를 들어 문자열, 숫자, 참과 거짓 같은 값이 원시 자료형에 해당한다.
Symbol은 고유한 값을 만들 때 사용한다.
특히Symbol()을 호출하면 매번 새로운 고유 값이 만들어진다.
겉으로는 같은 설명을 가진Symbol처럼 보여도, 실제로는 서로 다른 값이다.
예를 들어 객체에 새로운 프로퍼티를 추가해야 하는데, 기존 프로퍼티 이름과 충돌하면 문제가 생길 수 있다.
이때Symbol을 프로퍼티 이름으로 사용하면 다른 이름과 겹칠 가능성을 줄일 수 있다.
또Symbol프로퍼티는 일반적인 프로퍼티 목록 조회에서 잘 드러나지 않는다.
Symbol()의 핵심은 같은 설명을 넣어도 호출할 때마다 새로운 고유 값이 만들어진다는 점이다.
그래서 객체의 프로퍼티 이름 충돌을 줄이거나, 일반적인 조회에서 덜 드러나는 프로퍼티를 만들 때 사용할 수 있다.
Symbol은 고유한 값을 만든다
Symbol은Symbol()함수를 사용해서 만든다.
이때 괄호 안에 설명을 넣을 수 있다.
이 설명은 사람이 구분하기 쉽게 붙이는 이름일 뿐, 실제 값이 같아지는 기준은 아니다.
여기서 주의할 점은Symbol은new를 붙여 만들지 않는다는 것이다.
Promise나Proxy는new Promise(),new Proxy()처럼 만들지만,Symbol은Symbol()처럼 함수 호출 형태로 만든다.
아래 예제는 같은 설명을 가진Symbol두 개를 비교하는 코드이다.// symbol-unique-basic.js const symbol1 = Symbol("id"); // id 설명을 가진 Symbol 생성 const symbol2 = Symbol("id"); // 같은 설명을 가진 Symbol 생성 console.log(symbol1 === symbol2); // 두 Symbol이 같은지 비교 console.log(symbol1); // 첫 번째 Symbol 출력 console.log(symbol2); // 두 번째 Symbol 출력// 출력결과 // false // Symbol(id) // Symbol(id)
symbol1과symbol2는 둘 다Symbol("id")로 만들었다.
출력하면 둘 다Symbol(id)처럼 보인다.
하지만symbol1 === symbol2의 결과는false이다.
같은 설명을 넣어도Symbol()을 호출할 때마다 새로운 고유 값이 만들어지기 때문이다.
Symbol()은 호출할 때마다 새로운 고유 값을 만든다.
그래서 설명이 같아도 두Symbol은 같은 값이 아니다.
Symbol의 설명은 값을 구분하는 기준이 아니다
Symbol("id")에서"id"는 설명이다.
이 설명은 개발자가 어떤 용도로 만든Symbol인지 알아보기 쉽게 붙이는 글자이다.
하지만 설명이 같다고 해서 같은Symbol이 되는 것은 아니다.
Symbol의 고유성은 설명으로 결정되지 않는다.
Symbol()을 호출한 순간마다 서로 다른 값이 만들어진다.
아래 예제는 설명을 확인하는 코드이다.// symbol-description.js const userId = Symbol("userId"); // userId 설명을 가진 Symbol 생성 console.log(userId.description); // Symbol 설명 확인 console.log(String(userId)); // Symbol을 문자열 형태로 확인// 출력결과 // userId // Symbol(userId)
description은Symbol을 만들 때 넣은 설명을 확인할 때 사용한다.
String(userId)는Symbol을 문자열처럼 확인하기 위한 방법이다.
다만 여기서 중요한 점은description이 같다고 해서 같은 값이 되는 것이 아니라는 점이다.
설명은 말 그대로 사람이 알아보기 위한 표시이다.
정리하면 아래처럼 볼 수 있다.
Symbol("userId")에서"userId"는 설명이다.- 설명은 사람이 알아보기 위한 표시이다.
- 설명이 같아도
Symbol값은 서로 다를 수 있다.
Symbol의 설명은 고유 값을 만드는 기준이 아니라, 사람이 구분하기 쉽게 붙이는 표시이다.
Symbol은 객체의 프로퍼티 이름으로 사용할 수 있다
Symbol은 객체의 프로퍼티 이름으로 사용할 수 있다.
객체의 프로퍼티 이름은 보통 문자열을 사용하지만,Symbol도 프로퍼티 이름으로 사용할 수 있다.
이때Symbol을 프로퍼티 이름으로 쓰려면 대괄호[]를 사용해야 한다.
객체에서 대괄호를 사용하면 변수에 들어 있는 값을 프로퍼티 이름으로 사용할 수 있다.
아래 예제는Symbol을 객체의 프로퍼티 이름으로 사용하는 코드이다.// symbol-object-property.js const idKey = Symbol("id"); // 고유한 프로퍼티 이름으로 사용할 Symbol 생성 const user = { nickname: "짱구", // 일반 문자열 프로퍼티 [idKey]: 1001 // Symbol을 프로퍼티 이름으로 사용 }; console.log(user.nickname); // 일반 프로퍼티 값 출력 console.log(user[idKey]); // Symbol 프로퍼티 값 출력// 출력결과 // 짱구 // 1001
idKey는Symbol값이다.
객체 안에서[idKey]: 1001이라고 작성했기 때문에idKey에 들어 있는Symbol이 프로퍼티 이름으로 사용된다.
Symbol로 만든 프로퍼티는 점 표기법으로 접근할 수 없다.
user.idKey처럼 쓰면idKey라는 문자열 이름의 프로퍼티를 찾게 된다.
그래서Symbol프로퍼티에 접근할 때는user[idKey]처럼 대괄호를 사용해야 한다.
Symbol을 객체의 프로퍼티 이름으로 사용할 때는 반드시 대괄호[]로 접근해야 한다.
Symbol을 사용하면 프로퍼티 이름 충돌을 줄일 수 있다
객체에 여러 코드가 값을 추가하다 보면 같은 프로퍼티 이름을 사용할 수 있다.
이때 문자열 이름을 사용하면 기존 값이 덮어써질 수 있다.
아래 예제는 같은 문자열 프로퍼티 이름을 사용해서 값이 덮어써지는 경우이다.// symbol-key-conflict-string.js const user = { nickname: "짱구", // 닉네임 id: 1001 // 기존 id 값 }; user.id = "external-id"; // 같은 이름의 프로퍼티에 새 값 대입 console.log(user.id); // 덮어쓴 id 값 출력// 출력결과 // external-id처음
user.id에는1001이 들어 있었다.
그런데 다시user.id = "external-id"를 실행했기 때문에 기존 값이 덮어써졌다.
문자열 프로퍼티 이름은 이름이 같으면 같은 프로퍼티를 가리킨다.
그래서 의도하지 않은 충돌이 생길 수 있다.
이번에는Symbol을 사용해 보자.// symbol-key-conflict-symbol.js const userIdKey = Symbol("id"); // 내부 id용 Symbol const externalIdKey = Symbol("id"); // 외부 id용 Symbol const user = { nickname: "짱구", // 닉네임 [userIdKey]: 1001 // 내부 id 저장 }; user[externalIdKey] = "external-id"; // 다른 Symbol 프로퍼티에 외부 id 저장 console.log(user[userIdKey]); // 내부 id 출력 console.log(user[externalIdKey]); // 외부 id 출력 console.log(userIdKey === externalIdKey); // 두 Symbol 비교// 출력결과 // 1001 // external-id // false
userIdKey와externalIdKey는 둘 다 설명이"id"이다.
하지만 두 값은 서로 다른Symbol이다.
그래서 같은 객체 안에서 사용해도 서로 다른 프로퍼티 이름으로 동작한다.
기존userIdKey값이 덮어써지지 않고,externalIdKey값이 따로 저장된다.
Symbol을 프로퍼티 이름으로 사용하면 같은 설명을 가진 값이라도 서로 다른 키가 되기 때문에 이름 충돌을 줄일 수 있다.
Symbol 프로퍼티는 일반 반복에서 잘 보이지 않는다
Symbol로 만든 프로퍼티는 일반적인 객체 탐색에서 잘 보이지 않는다.
예를 들어Object.keys()나for...in은 문자열 프로퍼티 이름을 대상으로 한다.
그래서Symbol프로퍼티는 객체 안에 있지만, 일반 프로퍼티 목록에는 나오지 않는다.
이 점 때문에Symbol은 숨겨진 값처럼 보일 수 있다.
하지만 완전히 비공개 값은 아니다.
아래 예제를 보면 차이를 확인할 수 있다.// symbol-property-enumeration.js const idKey = Symbol("id"); // Symbol 프로퍼티 이름 생성 const user = { nickname: "짱구", // 일반 문자열 프로퍼티 age: 5, // 일반 문자열 프로퍼티 [idKey]: 1001 // Symbol 프로퍼티 }; console.log(Object.keys(user)); // 문자열 프로퍼티 이름 목록 출력 for (const key in user) { // 문자열 프로퍼티를 반복 console.log(key); // 반복되는 key 출력 } console.log(Object.getOwnPropertySymbols(user)); // Symbol 프로퍼티 목록 출력// 출력결과 // ['nickname', 'age'] // nickname // age // [Symbol(id)]
Object.keys(user)는nickname,age만 보여 준다.
for...in반복에서도nickname,age만 출력된다.
하지만Object.getOwnPropertySymbols(user)를 사용하면 객체 안의Symbol프로퍼티를 확인할 수 있다.
즉,Symbol프로퍼티가 완전히 사라진 것이 아니다.
일반적인 문자열 프로퍼티 목록에 포함되지 않을 뿐이다.
Symbol프로퍼티는 일반적인 프로퍼티 탐색에서 잘 드러나지 않지만, 완전히 숨겨진 비공개 값은 아니다.
필요하면Object.getOwnPropertySymbols()로 확인할 수 있다.
JSON.stringify는 Symbol 프로퍼티를 문자열로 바꾸지 않는다
객체를
JSON.stringify()로JSON문자열로 바꿀 때Symbol프로퍼티는 포함되지 않는다.
JSON은 데이터를 문자열 형식으로 표현하는 규칙이고,Symbol프로퍼티는 그 변환 대상에 포함되지 않는다.
아래 예제는Symbol프로퍼티가JSON문자열에 포함되지 않는 흐름을 보여 준다.// symbol-json-stringify.js const idKey = Symbol("id"); // Symbol 프로퍼티 이름 생성 const user = { nickname: "짱구", // 일반 프로퍼티 [idKey]: 1001 // Symbol 프로퍼티 }; const jsonText = JSON.stringify(user); // 객체를 JSON 문자열로 변환 console.log(jsonText); // JSON 변환 결과 출력// 출력결과 // {"nickname":"짱구"}
user객체에는nickname과Symbol(id)프로퍼티가 있다.
하지만JSON.stringify(user)결과에는nickname만 포함된다.
이 말은Symbol프로퍼티가 객체 안에 없는 것이 아니라,JSON문자열로 변환될 때 제외된다는 뜻이다.
이 흐름은 앞에서 본JSON의 특징과 연결된다.
JSON은 데이터를 주고받기 위한 문자열 형식이므로,Symbol처럼 고유한 프로퍼티 키는 일반적인JSON데이터로 표현되지 않는다.
Symbol.for는 전역 Symbol 저장소를 사용한다
일반적인
Symbol()은 호출할 때마다 새로운 고유 값을 만든다.
하지만 같은 이름으로 같은Symbol을 다시 찾아 사용해야 할 때도 있다.
이때 사용할 수 있는 것이Symbol.for()이다.
Symbol.for()는 전역Symbol저장소를 사용한다.
전역 저장소는 프로그램 전체에서 공유되는 보관 공간이라고 이해하면 된다.
Symbol.for("id")를 호출하면 먼저 전역 저장소에"id"라는 이름의Symbol이 있는지 찾는다.
이미 있으면 그 값을 그대로 가져오고, 없으면 새로 만들어 저장한 뒤 반환한다.
아래 예제는Symbol()과Symbol.for()의 차이를 보여 준다.// symbol-for-basic.js const symbol1 = Symbol("id"); // 매번 새로운 Symbol 생성 const symbol2 = Symbol("id"); // 매번 새로운 Symbol 생성 const globalSymbol1 = Symbol.for("id"); // 전역 저장소에서 id Symbol 생성 또는 조회 const globalSymbol2 = Symbol.for("id"); // 전역 저장소에서 같은 id Symbol 조회 console.log(symbol1 === symbol2); // 일반 Symbol 비교 console.log(globalSymbol1 === globalSymbol2); // 전역 Symbol 비교// 출력결과 // false // true
Symbol("id")는 호출할 때마다 새로운 값을 만든다.
그래서symbol1 === symbol2는false이다.
반면Symbol.for("id")는 전역 저장소에서 같은 이름의Symbol을 찾는다.
처음에는 없으므로 만들고, 두 번째 호출에서는 이미 만든 값을 가져온다.
그래서globalSymbol1 === globalSymbol2는true이다.
Symbol()은 매번 새로운 고유 값을 만들고,Symbol.for()는 전역 저장소에서 같은 이름의Symbol을 공유한다.
Symbol.keyFor는 전역 Symbol의 이름을 찾는다
Symbol.keyFor()는Symbol.for()로 만든 전역Symbol의 이름을 찾을 때 사용한다.
여기서 이름은Symbol.for("id")에 넣었던"id"같은 값을 말한다.
중요한 점은Symbol.keyFor()가 모든Symbol의 설명을 찾아 주는 것이 아니라는 점이다.
Symbol.for()로 전역 저장소에 등록된Symbol에 대해서만 이름을 찾을 수 있다.
아래 예제를 보면 차이를 확인할 수 있다.// symbol-keyfor-basic.js const localSymbol = Symbol("id"); // 일반 Symbol 생성 const globalSymbol = Symbol.for("id"); // 전역 Symbol 생성 또는 조회 console.log(Symbol.keyFor(localSymbol)); // 일반 Symbol은 전역 이름이 없음 console.log(Symbol.keyFor(globalSymbol)); // 전역 Symbol의 이름 조회// 출력결과 // undefined // id
localSymbol은Symbol("id")로 만든 일반Symbol이다.
전역 저장소에 등록된 값이 아니므로Symbol.keyFor(localSymbol)의 결과는undefined이다.
globalSymbol은Symbol.for("id")로 만든 전역Symbol이다.
그래서Symbol.keyFor(globalSymbol)을 실행하면 전역 저장소에 등록된 이름인"id"가 나온다.
Symbol.keyFor()는Symbol.for()로 등록된 전역Symbol의 이름을 찾을 때 사용한다.
일반Symbol()로 만든 값의 설명을 찾는 용도로 사용하는 것이 아니다.
Symbol 핵심 정리
Symbol은 고유한 값을 만들기 위한 원시 자료형이다.
특히Symbol()은 같은 설명을 넣어도 매번 새로운 값을 만들기 때문에, 객체의 프로퍼티 이름 충돌을 줄일 때 사용할 수 있다.
핵심은 아래처럼 정리할 수 있다.
Symbol은 고유한 값을 만드는 원시 자료형이다.Symbol()은 호출할 때마다 새로운 고유 값을 만든다.Symbol은new를 붙여 만들지 않는다.- 같은 설명을 넣어도 서로 다른
Symbol이 만들어진다.Symbol의 설명은 사람이 구분하기 위한 표시일 뿐, 같은 값을 만드는 기준이 아니다.Symbol은 객체의 프로퍼티 이름으로 사용할 수 있다.Symbol프로퍼티에 접근할 때는 대괄호[]를 사용한다.Symbol을 프로퍼티 이름으로 사용하면 이름 충돌을 줄일 수 있다.Symbol프로퍼티는Object.keys()나for...in에 드러나지 않는다.Symbol프로퍼티는 완전히 비공개 값은 아니다.Symbol프로퍼티는Object.getOwnPropertySymbols()로 확인할 수 있다.JSON.stringify()는Symbol프로퍼티를JSON문자열에 포함하지 않는다.Symbol.for()는 전역 저장소에서 같은 이름의Symbol을 공유한다.Symbol.keyFor()는Symbol.for()로 등록된 전역Symbol의 이름을 찾는다.
Symbol을 이해할 때는 “설명이 같은가”가 아니라 “같은Symbol값을 공유하는가”를 기준으로 봐야 한다.
이 기준을 잡으면Symbol(),Symbol.for(), 객체 프로퍼티 키로 사용하는 흐름을 더 쉽게 구분할 수 있다.
Set은 중복된 값을 허용하지 않는 데이터 묶음이다.
배열처럼 여러 값을 담을 수 있지만, 같은 값을 여러 번 저장하지 않는다는 점이 다르다.
예를 들어 같은 이름이 여러 번 들어 있는 목록에서 중복을 제거해야 한다고 생각해 보자.
배열만 사용하면 직접 반복하면서 이미 있는 값인지 확인해야 한다.
하지만Set을 사용하면 같은 값은 한 번만 저장되므로 중복 제거 흐름을 더 쉽게 만들 수 있다.
Set의 핵심은 여러 값을 저장하되, 같은 값은 하나만 유지한다는 점이다.
그래서 중복 없는 목록이 필요할 때 사용할 수 있다.
Set은 중복 값을 저장하지 않는다
Set은new Set()으로 만든다.
Set에 값을 추가할 때는add()를 사용한다.
같은 값을 여러 번 추가해도Set안에는 하나만 남는다.
즉,Set은 값을 추가할 때 이미 같은 값이 있는지 확인하고, 있으면 다시 저장하지 않는다.
아래 예제는 같은 값을 여러 번 추가하는 코드이다.// set-unique-basic.js const numbers = new Set(); // 빈 Set 생성 numbers.add(1); // 1 추가 numbers.add(2); // 2 추가 numbers.add(2); // 이미 있는 2 다시 추가 numbers.add(3); // 3 추가 numbers.add(3); // 이미 있는 3 다시 추가 console.log([...numbers]); // Set을 배열로 펼쳐 출력 console.log(numbers.size); // Set에 저장된 값 개수 출력// 출력결과 // [1, 2, 3] // 3
2와3을 두 번씩 추가했지만 결과에는 한 번씩만 들어 있다.
그래서Set을 배열로 바꿔 출력하면[1, 2, 3]이 나온다.
size는Set에 들어 있는 값의 개수를 나타내는 프로퍼티이다.
중복 값은 저장되지 않기 때문에size는3이다.
Set은 같은 값을 여러 번 추가해도 한 번만 저장한다.
이 점이 배열과 가장 큰 차이이다.
Set은 배열과 비슷하지만 인덱스로 접근하지 않는다
Set은 여러 값을 담는다는 점에서 배열과 비슷해 보인다.
하지만 배열과 같은 구조는 아니다.
배열은 값의 위치를 인덱스로 관리한다.
인덱스는 배열에서 값의 순서를 나타내는 번호이며,0부터 시작한다.
그래서 배열은arr[0],arr[1]처럼 위치로 값을 꺼낼 수 있다.
반면Set은 중복 없는 값의 묶음이다.
값이 들어간 순서는 유지되지만, 배열처럼 인덱스 번호로 직접 접근하지 않는다.
아래 예제를 보면 차이를 알 수 있다.// set-array-difference.js const fruitsArray = ["apple", "banana", "apple"]; // 중복이 있는 배열 const fruitsSet = new Set(["apple", "banana", "apple"]); // 중복이 제거되는 Set console.log(fruitsArray[0]); // 배열은 인덱스로 접근 가능 console.log(fruitsSet[0]); // Set은 인덱스로 접근하지 않음 console.log([...fruitsSet]); // Set을 배열로 바꿔 확인// 출력결과 // apple // undefined // ['apple', 'banana']배열인
fruitsArray는fruitsArray[0]으로 첫 번째 값을 꺼낼 수 있다.
하지만fruitsSet[0]은 사용할 수 없다.
Set은 배열처럼 인덱스 번호로 값을 꺼내는 구조가 아니기 때문이다.
Set안의 값을 배열처럼 보고 싶다면Spread Operator를 사용해[...fruitsSet]처럼 배열로 바꿀 수 있다.
배열은 순서와 인덱스를 기준으로 값을 다루고,Set은 중복 없는 값의 묶음을 기준으로 값을 다룬다.
add는 값을 추가한다
add()는Set에 값을 추가하는 메서드이다.
메서드는 객체가 가지고 있는 함수라고 이해하면 된다.
add()는 값을 추가한 뒤 다시 자기 자신인Set을 반환한다.
그래서add()를 이어서 사용할 수도 있다.
아래 예제는add()를 여러 번 이어서 사용하는 코드이다.// set-add-chain.js const animals = new Set(); // 빈 Set 생성 animals .add("dog") // dog 추가 .add("cat") // cat 추가 .add("dog") // 이미 있는 dog 다시 추가 .add("rabbit"); // rabbit 추가 console.log([...animals]); // Set을 배열로 바꿔 출력 console.log(animals.size); // 저장된 값 개수 출력// 출력결과 // ['dog', 'cat', 'rabbit'] // 3
dog를 두 번 추가했지만 한 번만 저장된다.
add()를 이어서 사용해도 결과는 중복 없는 값만 남는다.
정리하면add()는 값을 추가하는 메서드이고, 이미 있는 값은 다시 추가하지 않는다.
has는 값이 있는지 확인한다
has()는Set안에 특정 값이 있는지 확인하는 메서드이다.
값이 있으면true, 없으면false를 반환한다.
여기서true는 참,false는 거짓을 의미한다.
조건문에서 어떤 값이 있는지 검사할 때 자주 사용할 수 있다.
아래 예제는 특정 값이 있는지 확인하는 코드이다.// set-has-basic.js const fruits = new Set(["apple", "banana", "orange"]); // 과일 Set 생성 console.log(fruits.has("banana")); // banana가 있는지 확인 console.log(fruits.has("grape")); // grape가 있는지 확인// 출력결과 // true // false
banana는Set안에 있으므로true가 나온다.
grape는Set안에 없으므로false가 나온다.
has()는Set에 어떤 값이 포함되어 있는지 확인할 때 사용한다.
배열에서includes()로 포함 여부를 확인하는 것과 비슷한 역할로 이해하면 된다.
delete는 값을 삭제한다
delete()는Set에서 특정 값을 삭제하는 메서드이다.
삭제에 성공하면true, 삭제할 값이 없으면false를 반환한다.
아래 예제는Set에서 값을 삭제하는 코드이다.// set-delete-basic.js const fruits = new Set(["apple", "banana", "orange"]); // 과일 Set 생성 console.log(fruits.delete("banana")); // banana 삭제 시도 console.log([...fruits]); // 삭제 후 Set 확인 console.log(fruits.delete("grape")); // 없는 값 삭제 시도 console.log([...fruits]); // Set 상태 다시 확인// 출력결과 // true // ['apple', 'orange'] // false // ['apple', 'orange']
banana는Set안에 있었기 때문에 삭제되고true가 반환된다.
삭제 후에는apple,orange만 남는다.
반면grape는 처음부터 없던 값이다.
그래서 삭제할 수 없고false가 반환된다.
이처럼delete()는 값 삭제뿐 아니라 삭제가 실제로 되었는지도 알려 준다.
clear는 모든 값을 삭제한다
clear()는Set안의 모든 값을 한 번에 삭제하는 메서드이다.
특정 값 하나만 삭제하는delete()와 다르게,clear()는 전체를 비운다.
아래 예제는Set을 비우는 코드이다.// set-clear-basic.js const numbers = new Set([1, 2, 3, 4]); // 숫자 Set 생성 console.log(numbers.size); // 삭제 전 개수 확인 numbers.clear(); // 모든 값 삭제 console.log([...numbers]); // 비워진 Set 확인 console.log(numbers.size); // 삭제 후 개수 확인// 출력결과 // 4 // [] // 0처음에는
Set안에1,2,3,4가 들어 있으므로size는4이다.
clear()를 실행하면 모든 값이 삭제된다.
그래서 배열로 바꿔 출력하면 빈 배열[]이 나오고,size는0이 된다.
delete()는 특정 값 하나를 지우고,clear()는Set전체를 비운다.
두 메서드는 삭제 범위가 다르다.
Set은 반복해서 값을 꺼낼 수 있다
Set은 여러 값을 담고 있으므로 반복해서 값을 꺼낼 수 있다.
대표적으로for...of를 사용할 수 있다.
for...of는 배열이나Set처럼 여러 값을 가진 구조에서 값을 하나씩 꺼내는 반복문이다.
Set은 값이 추가된 순서를 유지하므로, 반복할 때도 추가된 순서대로 값이 나온다.
아래 예제는Set의 값을 하나씩 출력하는 코드이다.// set-for-of-basic.js const fruits = new Set(["apple", "banana", "orange"]); // 과일 Set 생성 for (const fruit of fruits) { // Set의 값을 하나씩 꺼냄 console.log(fruit); // 현재 값 출력 }// 출력결과 // apple // banana // orange
for...of는fruits안의 값을 하나씩 꺼낸다.
첫 번째 반복에서는"apple", 두 번째 반복에서는"banana", 세 번째 반복에서는"orange"가 출력된다.
이처럼Set은 배열처럼 인덱스로 접근하지는 않지만, 반복문으로 값을 하나씩 꺼낼 수 있다.
Set에서도 forEach를 사용할 수 있다
Set은forEach()메서드도 사용할 수 있다.
forEach()는 여러 값을 하나씩 꺼내면서 전달한 함수를 실행하는 메서드이다.
배열의forEach()와 비슷하게 동작하지만,Set에는 배열 인덱스가 없다.
그래서Set에서는 현재 값 자체를 중심으로 처리한다고 이해하면 된다.
아래 예제는Set의 값을forEach()로 출력하는 코드이다.// set-foreach-basic.js const numbers = new Set([10, 20, 30]); // 숫자 Set 생성 numbers.forEach((number) => { // Set의 값을 하나씩 꺼내 실행 console.log(number); // 현재 값 출력 });// 출력결과 // 10 // 20 // 30
forEach()는Set안의 값을 순서대로 꺼낸다.
각 값은number에 들어가고,console.log(number)가 실행된다.
Set은 중복 값을 저장하지 않기 때문에, 반복할 때도 중복 없는 값만 나온다.
Set은 배열의 중복을 제거할 때 자주 사용한다
Set은 중복 값을 허용하지 않는다.
그래서 배열에서 중복을 제거할 때 자주 사용한다.
흐름은 간단하다.
먼저 배열을Set으로 바꾸면 중복이 제거된다.
그다음Spread Operator로 다시 배열로 펼치면 중복 없는 배열을 만들 수 있다.
아래 예제는 숫자 배열의 중복을 제거하는 코드이다.// set-remove-duplicates.js const numbers = [1, 2, 2, 3, 3, 4, 5, 5]; // 중복이 있는 배열 const uniqueNumbers = [...new Set(numbers)]; // Set으로 중복 제거 후 배열로 변환 console.log(uniqueNumbers); // 중복 제거 결과 출력// 출력결과 // [1, 2, 3, 4, 5]
new Set(numbers)를 실행하면 배열의 값들이Set에 들어간다.
이때 중복된2,3,5는 한 번씩만 저장된다.
그다음[...new Set(numbers)]는Set안의 값을 펼쳐 새 배열로 만든다.
그래서uniqueNumbers에는 중복이 제거된[1, 2, 3, 4, 5]가 들어간다.
배열의 중복을 제거할 때는Set으로 바꾼 뒤 다시 배열로 펼치는 방식을 사용할 수 있다.
이 흐름은 앞에서 배운Spread Operator와 자연스럽게 연결된다.
Set에서 객체는 내용이 같아도 다른 값으로 본다
Set은 중복 값을 저장하지 않는다.
하지만 객체를 저장할 때는 주의해야 한다.
객체는 안의 내용이 같아 보여도, 서로 다른 객체로 만들어졌다면 다른 값으로 취급된다.
즉,{ id: 1 }과{ id: 1 }은 모양은 같지만 서로 다른 객체이다.
아래 예제는 내용이 같은 객체 두 개를Set에 추가하는 코드이다.// set-object-reference.js const user1 = { id: 1, nickname: "짱구" }; // 첫 번째 객체 const user2 = { id: 1, nickname: "짱구" }; // 내용은 같지만 새로 만든 두 번째 객체 const users = new Set(); // 빈 Set 생성 users.add(user1); // 첫 번째 객체 추가 users.add(user2); // 두 번째 객체 추가 console.log(users.size); // Set에 저장된 객체 개수 확인// 출력결과 // 2
user1과user2는 내용이 같다.
하지만 각각 따로 만든 객체이다.
그래서Set은 이 둘을 같은 값으로 보지 않는다.
Set은 객체의 내부 내용을 비교해서 중복을 판단하지 않는다.
객체가 같은 참조인지, 즉 같은 객체 자체인지를 기준으로 본다.
Set은 숫자나 문자열 같은 원시값은 값 자체로 중복을 판단하지만, 객체는 같은 객체를 가리키는지로 판단한다.
이 점을 모르면 객체 배열에서 중복 제거가 예상대로 되지 않을 수 있다.
Set 핵심 정리
Set은 중복 없는 값을 저장하기 위한 데이터 구조이다.
배열처럼 여러 값을 담을 수 있지만, 배열처럼 인덱스로 접근하지 않고 같은 값을 중복 저장하지 않는다.
핵심은 아래처럼 정리할 수 있다.
Set은 중복 값을 허용하지 않는 데이터 묶음이다.new Set()으로Set을 만들 수 있다.add()는 값을 추가한다.has()는 특정 값이 있는지 확인한다.delete()는 특정 값을 삭제한다.clear()는 모든 값을 삭제한다.size는Set에 저장된 값의 개수를 나타낸다.Set은 값이 추가된 순서를 유지한다.Set은 배열처럼 인덱스로 접근하지 않는다.for...of나forEach()로 값을 반복해서 꺼낼 수 있다.- 배열을
Set으로 바꾸면 중복 값을 제거할 수 있다.Spread Operator를 사용하면Set을 다시 배열로 바꿀 수 있다.- 객체는 내용이 같아도 서로 다른 객체이면
Set에서 다른 값으로 취급된다.
Set을 이해할 때는 “여러 값을 담는다”보다 “중복을 허용하지 않는다”는 특징을 먼저 봐야 한다.
이 기준을 잡으면 배열과Set의 차이, 중복 제거 흐름, 객체 중복 판단 기준까지 자연스럽게 이어서 이해할 수 있다.
Map은key와value를 한 쌍으로 저장하는 데이터 구조이다.
여기서key는 값을 찾기 위한 기준이고,value는 실제로 저장되는 값이다.
객체도key와value형태로 값을 저장할 수 있다.
하지만Map은 객체보다 더 명확하게 “값을 저장하고,key로 다시 찾는 구조”를 제공한다.
또Map의key에는 문자열뿐 아니라 숫자, 객체, 함수 같은 값도 사용할 수 있다.
예를 들어 회원 번호로 회원 이름을 찾거나, 상품 이름으로 가격을 찾거나, 객체 자체를 기준으로 데이터를 저장해야 할 때Map을 사용할 수 있다.
Map의 핵심은key와value를 한 쌍으로 저장하고,key를 기준으로value를 꺼내는 것이다.
배열이 여러 값을 순서대로 담는 구조라면,Map은 특정 기준으로 값을 찾아 쓰는 구조라고 이해하면 된다.
Map은 key와 value를 한 쌍으로 저장한다
Map은new Map()으로 만든다.
처음에는 비어 있는Map을 만들고,set()으로 값을 추가할 수 있다.
set()에는 두 값이 들어간다.
첫 번째 값은key이고, 두 번째 값은value이다.
즉,set(key, value)형태로 저장한다.
아래 예제는 회원 번호를key로 사용하고, 회원 이름을value로 저장하는 코드이다.// map-key-value-basic.js const members = new Map(); // 빈 Map 생성 members.set(1, "짱구"); // key 1에 value 짱구 저장 members.set(2, "철수"); // key 2에 value 철수 저장 members.set(3, "유리"); // key 3에 value 유리 저장 console.log(members.get(1)); // key 1의 value 조회 console.log(members.get(2)); // key 2의 value 조회 console.log(members.get(3)); // key 3의 value 조회// 출력결과 // 짱구 // 철수 // 유리
members.set(1, "짱구")는1이라는key에"짱구"라는value를 저장한다.
나중에members.get(1)을 실행하면key가1인 값을 찾아"짱구"를 반환한다.
이처럼Map은 값을 순서대로 꺼내는 것보다, 특정key를 기준으로 값을 찾는 데 초점이 있다.
Map은 값을 저장할 때도key와value를 함께 저장하고, 값을 꺼낼 때도key를 사용한다.
set은 값을 저장하고 같은 key의 value를 바꾼다
set()은Map에 값을 저장하는 메서드이다.
처음 사용하는key라면 새로운 값이 추가된다.
이미 존재하는key라면 기존value가 새value로 바뀐다.
즉,Map에서는 같은key가 여러 개 존재하지 않는다.
하나의key는 하나의value만 가질 수 있다.
아래 예제는 같은key에 값을 다시 저장하는 코드이다.// map-set-overwrite.js const scores = new Map(); // 빈 Map 생성 scores.set("짱구", 80); // 짱구 점수 저장 scores.set("철수", 90); // 철수 점수 저장 scores.set("짱구", 100); // 같은 key인 짱구의 value 변경 console.log(scores.get("짱구")); // 짱구 점수 조회 console.log(scores.get("철수")); // 철수 점수 조회 console.log(scores.size); // Map에 저장된 key 개수 확인// 출력결과 // 100 // 90 // 2처음에는
"짱구"라는key에80이 저장되었다.
그런데 다시scores.set("짱구", 100)을 실행했기 때문에"짱구"의value가100으로 바뀐다.
"짱구"라는key가 두 개 생기는 것이 아니다.
같은key는 하나만 유지되고, 그key의value가 바뀐다.
그래서size는3이 아니라2이다.
Map에서 같은key로 다시set()을 실행하면 새 항목이 추가되는 것이 아니라 기존value가 변경된다.
get은 key로 value를 꺼낸다
get()은Map에서 특정key에 연결된value를 꺼내는 메서드이다.
get(key)형태로 사용한다.
해당key가 있으면 연결된value를 반환한다.
해당key가 없으면undefined를 반환한다.
여기서undefined는 값이 정의되어 있지 않다는 뜻이다.
아래 예제는 존재하는key와 존재하지 않는key를 조회하는 코드이다.// map-get-basic.js const prices = new Map(); // 빈 Map 생성 prices.set("apple", 1000); // apple 가격 저장 prices.set("banana", 1500); // banana 가격 저장 console.log(prices.get("apple")); // 존재하는 key 조회 console.log(prices.get("banana")); // 존재하는 key 조회 console.log(prices.get("orange")); // 존재하지 않는 key 조회// 출력결과 // 1000 // 1500 // undefined
"apple"과"banana"는Map에 저장된key이다.
그래서 각각1000,1500이 반환된다.
하지만"orange"는 저장된key가 아니다.
그래서prices.get("orange")의 결과는undefined이다.
get()은key를 기준으로value를 꺼내고, 없는key를 조회하면undefined를 반환한다.
has는 key가 있는지 확인한다
has()는Map안에 특정key가 있는지 확인하는 메서드이다.
있으면true, 없으면false를 반환한다.
get()으로도 값을 꺼낼 수 있지만, 값이 없는지 확인할 때는has()를 사용하는 것이 더 분명하다.
특히value자체가undefined일 수도 있는 상황에서는has()로key존재 여부를 확인하는 편이 안전하다.
아래 예제는Map에 특정key가 있는지 확인하는 코드이다.// map-has-basic.js const members = new Map(); // 빈 Map 생성 members.set("user1", "짱구"); // user1 저장 members.set("user2", "철수"); // user2 저장 console.log(members.has("user1")); // user1 key 존재 여부 확인 console.log(members.has("user3")); // user3 key 존재 여부 확인// 출력결과 // true // false
"user1"은Map에 저장된key이다.
그래서members.has("user1")은true를 반환한다.
반면"user3"은 저장된key가 아니다.
그래서members.has("user3")은false를 반환한다.
has()는Map에서 특정key가 존재하는지 확인할 때 사용한다.
값을 꺼내는 목적이면get(), 존재 여부를 확인하는 목적이면has()를 사용하면 된다.
delete는 key와 value 쌍을 삭제한다
delete()는Map에서 특정key에 해당하는 데이터를 삭제하는 메서드이다.
Map은key와value를 한 쌍으로 저장하기 때문에,key를 삭제하면 연결된value도 함께 사라진다.
삭제에 성공하면true를 반환한다.
삭제할key가 없으면false를 반환한다.
아래 예제는Map에서 데이터를 삭제하는 코드이다.// map-delete-basic.js const members = new Map(); // 빈 Map 생성 members.set(1, "짱구"); // key 1 저장 members.set(2, "철수"); // key 2 저장 console.log(members.delete(1)); // key 1 삭제 console.log(members.get(1)); // 삭제된 key 조회 console.log(members.size); // 삭제 후 개수 확인 console.log(members.delete(3)); // 없는 key 삭제 시도// 출력결과 // true // undefined // 1 // false
members.delete(1)은key가1인 데이터를 삭제한다.
삭제에 성공했기 때문에true가 반환된다.
삭제 후members.get(1)을 실행하면 더 이상 값이 없으므로undefined가 나온다.
그리고 남은 데이터는key가2인"철수"하나뿐이므로size는1이다.
마지막의members.delete(3)은 존재하지 않는key를 삭제하려고 한 것이다.
삭제할 데이터가 없으므로false가 반환된다.
clear는 Map 안의 모든 값을 삭제한다
clear()는Map안에 저장된 모든key와value쌍을 한 번에 삭제한다.
특정key하나만 삭제하는delete()와 다르게,clear()는 전체를 비운다.
아래 예제는Map전체를 비우는 코드이다.// map-clear-basic.js const members = new Map(); // 빈 Map 생성 members.set(1, "짱구"); // key 1 저장 members.set(2, "철수"); // key 2 저장 members.set(3, "유리"); // key 3 저장 console.log(members.size); // 삭제 전 개수 확인 members.clear(); // 전체 삭제 console.log(members.size); // 삭제 후 개수 확인 console.log([...members]); // 비워진 Map 확인// 출력결과 // 3 // 0 // []처음에는 세 개의 데이터가 저장되어 있으므로
size는3이다.
clear()를 실행하면 모든 데이터가 삭제된다.
그래서size는0이 되고,Map을 배열로 펼쳐 확인하면 빈 배열[]이 나온다.
delete()는 특정key하나를 삭제하고,clear()는Map전체를 비운다.
size는 Map에 저장된 데이터 개수를 나타낸다
size는Map에 저장된key와value쌍의 개수를 나타내는 프로퍼티이다.
배열에서 길이를 확인할 때length를 사용한다면,Map에서는size를 사용한다.
아래 예제는 값이 추가되고 삭제될 때size가 어떻게 바뀌는지 보여 준다.// map-size-basic.js const map = new Map(); // 빈 Map 생성 console.log(map.size); // 처음 개수 확인 map.set("a", 10); // a 저장 map.set("b", 20); // b 저장 console.log(map.size); // 저장 후 개수 확인 map.delete("a"); // a 삭제 console.log(map.size); // 삭제 후 개수 확인// 출력결과 // 0 // 2 // 1처음에는 저장된 값이 없으므로
size는0이다.
"a","b"를 저장하면 데이터가 두 개가 되므로size는2가 된다.
그다음"a"를 삭제하면 데이터가 하나만 남기 때문에size는1이 된다.
Map에 저장된 데이터 개수를 확인할 때는length가 아니라size를 사용한다.
Map은 추가된 순서대로 반복된다
Map은 데이터를 저장한 순서를 유지한다.
그래서 반복할 때도 추가된 순서대로key와value가 나온다.
Map은for...of로 반복할 수 있다.
이때 각 반복에서[key, value]형태의 배열이 하나씩 나온다.
아래 예제는Map을 반복하면서key와value를 출력하는 코드이다.// map-for-of-basic.js const members = new Map(); // 빈 Map 생성 members.set(1, "짱구"); // 첫 번째 저장 members.set(2, "철수"); // 두 번째 저장 members.set(3, "유리"); // 세 번째 저장 for (const [id, nickname] of members) { // key와 value를 구조분해할당으로 꺼냄 console.log(`${id}: ${nickname}`); // key와 value 출력 }// 출력결과 // 1: 짱구 // 2: 철수 // 3: 유리
members에는1,2,3순서로 데이터가 저장되었다.
그래서 반복할 때도 같은 순서로 값이 나온다.
for (const [id, nickname] of members)에서[id, nickname]은 구조분해할당이다.
Map에서 나온[key, value]배열을 바로id와nickname으로 나누어 받는 것이다.
Map은 저장된 순서를 유지하며, 반복할 때[key, value]형태로 값을 꺼낼 수 있다.
keys, values, entries로 원하는 부분만 꺼낼 수 있다
Map에서는key만 꺼내거나,value만 꺼내거나, 둘을 함께 꺼낼 수 있다.
이때 사용하는 메서드가keys(),values(),entries()이다.
각 역할은 아래와 같다.
keys()는Map의key만 꺼낸다.values()는Map의value만 꺼낸다.entries()는key와value쌍을 함께 꺼낸다.아래 예제는 세 메서드의 차이를 보여 준다.
// map-keys-values-entries.js const members = new Map(); // 빈 Map 생성 members.set(1, "짱구"); // key 1, value 짱구 members.set(2, "철수"); // key 2, value 철수 members.set(3, "유리"); // key 3, value 유리 console.log([...members.keys()]); // key만 배열로 확인 console.log([...members.values()]); // value만 배열로 확인 console.log([...members.entries()]); // key와 value 쌍을 배열로 확인// 출력결과 // [1, 2, 3] // ['짱구', '철수', '유리'] // [[1, '짱구'], [2, '철수'], [3, '유리']]
keys()는1,2,3처럼key만 꺼낸다.
values()는"짱구","철수","유리"처럼value만 꺼낸다.
entries()는[1, "짱구"],[2, "철수"],[3, "유리"]처럼key와value를 한 쌍으로 꺼낸다.
그리고Map자체를 반복하면 기본적으로entries()와 같은 흐름으로 동작한다.
Map에서도 forEach를 사용할 수 있다
Map은forEach()메서드도 사용할 수 있다.
forEach()는 저장된 데이터를 하나씩 꺼내면서 전달한 함수를 실행한다.
Map의forEach()에서 첫 번째 매개변수는value이고, 두 번째 매개변수는key이다.
배열의forEach()에서는 첫 번째가 현재 요소, 두 번째가 인덱스였기 때문에 헷갈릴 수 있다.
Map에서는value,key순서라는 점을 주의해야 한다.
아래 예제는Map의forEach()로 데이터를 출력하는 코드이다.// map-foreach-basic.js const members = new Map(); // 빈 Map 생성 members.set(1, "짱구"); // key 1 저장 members.set(2, "철수"); // key 2 저장 members.set(3, "유리"); // key 3 저장 members.forEach((nickname, id) => { // 첫 번째는 value, 두 번째는 key console.log(`${id}: ${nickname}`); // key와 value 출력 });// 출력결과 // 1: 짱구 // 2: 철수 // 3: 유리
members.forEach((nickname, id) => {})에서nickname은value이고,id는key이다.
그래서 출력하면1: 짱구,2: 철수,3: 유리순서로 나온다.
Map의forEach()는 매개변수 순서가value,key순서이다.
이 순서를 배열의forEach()와 헷갈리지 않아야 한다.
Map은 객체도 key로 사용할 수 있다
Map의 큰 특징 중 하나는 객체도key로 사용할 수 있다는 점이다.
일반 객체에서는 프로퍼티 이름이 보통 문자열이나Symbol로 처리된다.
하지만Map은 객체 자체를key로 사용할 수 있다.
아래 예제는 객체를key로 사용해서 값을 저장하는 코드이다.// map-object-key-basic.js const user1 = { id: 1, nickname: "짱구" }; // 첫 번째 사용자 객체 const user2 = { id: 2, nickname: "철수" }; // 두 번째 사용자 객체 const loginTimes = new Map(); // 로그인 시간 저장용 Map loginTimes.set(user1, "09:00"); // user1 객체를 key로 사용 loginTimes.set(user2, "10:00"); // user2 객체를 key로 사용 console.log(loginTimes.get(user1)); // user1 객체로 value 조회 console.log(loginTimes.get(user2)); // user2 객체로 value 조회// 출력결과 // 09:00 // 10:00이 예제에서
user1과user2는 객체이다.
Map은 이 객체들을key로 사용하고, 각각 로그인 시간을value로 저장한다.
나중에loginTimes.get(user1)처럼 같은 객체를key로 전달하면 연결된 값을 꺼낼 수 있다.
Map은 문자열뿐 아니라 객체 같은 값도key로 사용할 수 있다.
이 점이 일반 객체와Map을 구분하는 중요한 차이이다.
객체 key는 같은 내용을 가진 객체라도 같은 key가 아니다
Map에서 객체를key로 사용할 때는 주의할 점이 있다.
객체는 내용이 같아 보여도, 서로 따로 만든 객체라면 다른key로 취급된다.
즉,Map은 객체의 내부 내용을 비교해서 같은지 판단하지 않는다.
같은 객체를 가리키고 있는지를 기준으로 판단한다.
아래 예제는 내용이 같은 객체 두 개를key로 사용하는 코드이다.// map-object-key-reference.js const user1 = { id: 1, nickname: "짱구" }; // 첫 번째 객체 const user2 = { id: 1, nickname: "짱구" }; // 내용은 같지만 다른 객체 const map = new Map(); // 빈 Map 생성 map.set(user1, "첫 번째 객체의 값"); // user1을 key로 저장 console.log(map.get(user1)); // 같은 객체로 조회 console.log(map.get(user2)); // 내용만 같은 다른 객체로 조회 console.log(user1 === user2); // 두 객체가 같은 객체인지 비교// 출력결과 // 첫 번째 객체의 값 // undefined // false
user1과user2는id와nickname이 같다.
하지만 각각 따로 만든 객체이다.
그래서user1 === user2의 결과는false이다.
map.set(user1, "첫 번째 객체의 값")으로 저장했기 때문에 같은 객체인user1로 조회하면 값이 나온다.
하지만 내용만 같은user2로 조회하면 저장된key가 아니므로undefined가 나온다.
Map에서 객체를key로 사용할 때는 객체의 내용이 아니라 같은 객체를 가리키는지가 기준이다.
이 점은 앞에서 본Set의 객체 중복 판단과 같은 흐름으로 이해할 수 있다.
Object와 Map은 key 사용 방식이 다르다
Object와Map은 둘 다key와value형태로 데이터를 저장할 수 있다.
그래서 처음에는 비슷해 보인다.
하지만key를 다루는 방식이 다르다.
Object의 프로퍼티 이름은 보통 문자열이나Symbol이다.
숫자를key처럼 사용해도 내부적으로는 문자열 프로퍼티 이름처럼 다루어진다.
반면Map은key로 다양한 값을 사용할 수 있다.
문자열, 숫자, 객체, 함수,Symbol같은 값도key가 될 수 있다.
아래 예제는Object와Map에서 숫자key를 사용하는 차이를 보여 준다.// map-object-key-difference.js const objectData = {}; // 일반 객체 생성 const mapData = new Map(); // Map 생성 objectData[1] = "객체의 숫자 key"; // 객체에 숫자처럼 보이는 key 저장 mapData.set(1, "Map의 숫자 key"); // Map에 숫자 key 저장 console.log(objectData["1"]); // 객체에서는 문자열 key로 접근 console.log(mapData.get(1)); // Map에서는 숫자 key로 접근 console.log(mapData.get("1")); // 문자열 1로 조회// 출력결과 // 객체의 숫자 key // Map의 숫자 key // undefined일반 객체에서
objectData[1]로 값을 저장하면 실제로는"1"이라는 문자열 프로퍼티 이름처럼 접근할 수 있다.
그래서objectData["1"]로 값을 꺼낼 수 있다.
하지만Map에서는 숫자1과 문자열"1"이 다른key이다.
mapData.get(1)은 값을 찾지만,mapData.get("1")은 다른key를 찾는 것이므로undefined가 나온다.
Object는 프로퍼티 이름이 주로 문자열이나Symbol로 다루어지고,Map은 다양한 값을 그대로key로 사용할 수 있다.
Map 핵심 정리
Map은key와value를 한 쌍으로 저장하는 데이터 구조이다.
객체처럼key로 값을 찾을 수 있지만,key로 사용할 수 있는 값의 범위가 더 넓다.
핵심은 아래처럼 정리할 수 있다.
Map은key와value를 한 쌍으로 저장한다.new Map()으로Map을 만들 수 있다.set(key, value)는 값을 저장한다.- 같은
key로 다시set()하면 기존value가 바뀐다.get(key)는key에 연결된value를 꺼낸다.- 없는
key를get()하면undefined가 나온다.has(key)는 특정key가 있는지 확인한다.delete(key)는 특정key와 연결된 값을 삭제한다.clear()는Map전체를 비운다.size는 저장된key와value쌍의 개수이다.Map은 추가된 순서를 유지하며 반복된다.keys()는key만 꺼낸다.values()는value만 꺼낸다.entries()는key와value쌍을 꺼낸다.Map의forEach()는 매개변수 순서가value,key이다.Map은 객체도key로 사용할 수 있다.- 객체를
key로 사용할 때는 내용이 아니라 같은 객체를 가리키는지가 기준이다.Map에서는 숫자1과 문자열"1"이 서로 다른key이다.
Map을 이해할 때는 “순서대로 값을 담는 배열”이 아니라 “key로value를 찾는 저장소”라고 보는 것이 중요하다.
이 기준을 잡으면Object와의 차이, 객체key사용, 반복 메서드까지 자연스럽게 이해할 수 있다.