
이 예제는 기존 함수 표현식과 화살표 함수를 비교해서, 같은 동작을 더 짧게 작성하는 흐름을 보여 준다.
여기서 기존 함수 표현식은 이름이 없는 익명 함수를 변수에 저장한 방식이다.
두 함수는 모두 숫자 두 개를 받아 더한 결과를 반환한다.
하지만 함수를 만드는 방식과 결과를 반환하는 방식이 다르다.
먼저 예제 전체 코드를 보고, 그다음 코드를 기능별로 나누어 다시 설명한다.
응용예제에서는 전체 코드 구조를 먼저 본 뒤, 핵심 코드만 잘라서 다시 보는 방식이 가장 이해하기 쉽다.
예제 전체 코드
// exam1.html <!doctype html> <html> <head> <meta charset="UTF-8" /> <title>ECMAScript6</title> </head> <body> <h1>화살표 함수</h1> <hr /> <!-- 화살표 함수의 사용 가능 범위와 주의점을 화면에 보여 주는 부분 --> <ul> <li>클래스의 메서드로 사용하는 것은 가능</li> <li>화살표 함수는 생성자로 사용할 수 없음(자신만의 this가 없기 때문)</li> </ul> <!-- 화살표 함수의 기본 작성 형태를 정리한 부분 --> <pre> () => expression param => expression (param) => expression (param1, paramN) => expression () => { statements } param => { statements } (param1, paramN) => { statements } </pre> <script> // 1. 기존 함수 표현식 코드 // function 뒤에 이름이 없으므로 익명 함수이다. // 이 익명 함수를 변수 f에 저장한다. const f = function (a, b) { // a와 b를 더한 결과를 함수 밖으로 반환한다. return a + b; }; // 변수 f에 저장된 함수를 호출하고 반환값을 출력한다. console.log(f(100, 5)); // 2. 화살표 함수 코드 // function 키워드 대신 => 기호를 사용한다. // 중괄호 없이 표현식 하나를 쓰면 결과가 자동으로 반환된다. const af = (a, b) => a + b; // 변수 af에 저장된 화살표 함수를 호출하고 반환값을 출력한다. console.log(af(100, 5)); </script> </body> </html>이 코드는 크게 세 부분으로 나누어 볼 수 있다.
첫 번째는 화면에 화살표 함수의 주의점을 보여 주는ul부분이다.
두 번째는 화살표 함수의 여러 작성 형태를 보여 주는pre부분이다.
세 번째는 실제로 기존 함수 표현식과 화살표 함수를 비교하는script부분이다.
이 예제에서 가장 중요한 부분은script안의 두 함수이다.
f는 이름 없는 익명 함수를 변수에 저장한 기존 함수 표현식이다.
af는 같은 기능을 화살표 함수로 작성한 코드이다.
이 예제의 핵심은 익명 함수를 변수에 저장한 기존 함수 표현식과 화살표 함수가 같은 결과를 만들지만, 작성 방식은 다르다는 점이다.
이 예제에서 확인할 핵심
- 기존 함수 표현식은 함수를 값처럼 만들어 변수에 저장한다.
function (a, b) { ... }처럼 함수 이름이 없는 함수는 익명 함수이다.const f = function (a, b) { ... }는 익명 함수를 변수f에 저장한 코드이다.- 화살표 함수는
function키워드 대신=>기호를 사용한다.- 매개변수는 함수가 실행될 때 바깥에서 값을 받아 오는 자리이다.
return은 함수가 만든 결과를 함수 밖으로 돌려주는 문법이다.- 화살표 함수에서 중괄호 없이 표현식 하나를 쓰면 그 결과가 자동으로 반환된다.
this는 현재 코드가 어떤 객체를 기준으로 실행되는지 가리키는 값이다.- 화살표 함수는 자기만의
this를 새로 만들지 않는다.- 화살표 함수는
new로 호출되는 생성자 함수로 사용할 수 없다.이 예제는 단순히 함수 두 개를 비교하는 예제가 아니다.
기존 방식에서는 익명 함수를 변수에 담아 호출하고, 화살표 함수에서는 같은 동작을 더 짧게 표현하는 흐름을 보여 준다.
기존 함수 표현식과 익명 함수
// exam1.html // 기존 함수 표현식 코드 // function 뒤에 이름이 없으므로 익명 함수이다. // 이 익명 함수를 변수 f에 저장한다. const f = function (a, b) { // a와 b를 더한 결과를 함수 밖으로 반환한다. return a + b; }; console.log(f(100, 5)); // 변수 f에 저장된 함수를 호출한다.이 코드는 기존 함수 표현식이다.
함수 표현식은 함수를 값처럼 만들어 변수에 저장하는 방식이다.
여기서는function (a, b) { ... }라는 함수를f라는 변수에 저장했다.
여기서 중요한 점은function뒤에 함수 이름이 없다는 것이다.
그래서 이 함수는 익명 함수이다.
익명 함수는 이름이 없는 함수라는 뜻이다.
하지만 함수에 이름이 없다고 해서 호출할 수 없는 것은 아니다.
이 함수는f라는 변수에 저장되어 있다.
그래서f(100, 5)처럼 변수 이름을 사용해서 함수를 실행할 수 있다.
즉, 이 코드는 아래 흐름으로 동작한다.
- 이름 없는 익명 함수가 만들어진다.
- 그 익명 함수가 변수
f에 저장된다.f(100, 5)를 실행하면 변수f에 저장된 함수가 호출된다.100은 매개변수a에 들어간다.5는 매개변수b에 들어간다.return a + b로105가 반환된다.
const f = function (a, b) { ... }는 익명 함수를 변수f에 저장한 기존 함수 표현식이다.
이 부분을 놓치면 함수 선언문과 함수 표현식을 헷갈리기 쉽다.
함수 선언문과 함수 표현식은 다르다
함수 선언문은 함수 자체에 이름을 붙여 만드는 방식이다.
아래 코드는 원본 예제에 있는 코드는 아니고, 함수 표현식과 비교하기 위한 예시이다.// exam1.html // 비교용 예시 function f(a, b) { // 함수 자체에 f라는 이름이 붙어 있다. return a + b; }이 코드는 함수 자체에
f라는 이름이 붙어 있다.
그래서 함수 선언문이라고 볼 수 있다.
반면 원본 예제 코드는 아래처럼 생겼다.// exam1.html const f = function (a, b) { // 함수 이름이 없는 익명 함수를 변수 f에 저장한다. return a + b; };여기서는
function뒤에 이름이 없다.
대신 함수 전체를 변수f에 저장한다.
그래서 이 코드는 함수 선언문이 아니라 함수 표현식이다.
정리하면 차이는 이렇다.
- 함수 선언문은 함수 자체에 이름을 붙인다.
- 함수 표현식은 함수를 값처럼 만들어 변수에 저장한다.
- 예제의
function (a, b)는 이름이 없으므로 익명 함수이다.- 예제의
f는 함수 이름이라기보다 익명 함수가 저장된 변수 이름이다.이 예제에서는 함수 선언문이 아니라, 익명 함수를 변수에 저장한 함수 표현식을 화살표 함수와 비교한다.
화살표 함수 코드
// exam1.html // 화살표 함수 코드 // function 키워드 대신 => 기호를 사용한다. // 중괄호 없이 표현식 하나를 쓰면 결과가 자동으로 반환된다. const af = (a, b) => a + b; console.log(af(100, 5)); // 변수 af에 저장된 화살표 함수를 호출한다.이 코드는 기존 함수 표현식과 같은 기능을 화살표 함수로 작성한 것이다.
af라는 변수에 화살표 함수를 저장했다.
(a, b)는 함수가 받을 매개변수이다.
=>는 화살표 함수임을 나타내는 기호이다.
a + b는 실행하면 하나의 값으로 바뀌는 표현식이다.
여기서는 중괄호가 없다.
그래서a + b의 결과가 자동으로 반환된다.
즉,af(100, 5)를 실행하면100 + 5가 계산되고, 그 결과인105가 반환된다.
화살표 함수에서 중괄호 없이 표현식 하나를 쓰면 그 표현식의 결과가 자동으로 반환된다.
이 점이 기존 함수 표현식과 가장 먼저 비교해야 할 핵심이다.
기존 함수 표현식과 화살표 함수 비교
// exam1.html // 기존 함수 표현식 // 익명 함수를 변수 f에 저장한다. const f = function (a, b) { return a + b; }; // 화살표 함수 // 같은 기능을 더 짧게 작성한다. const af = (a, b) => a + b;두 함수는 모두
a와b를 받아 더한 결과를 반환한다.
하지만 작성 방식이 다르다.
기존 함수 표현식은function키워드를 사용한다.
그리고 중괄호 안에서return을 직접 작성해야 한다.
화살표 함수는function키워드 대신=>를 사용한다.
또 중괄호 없이 표현식 하나를 바로 작성하면 그 결과가 자동으로 반환된다.
그래서 같은 동작을 더 짧게 표현할 수 있다.// 출력결과 // 105 // 105첫 번째
105는f(100, 5)의 결과이다.
두 번째105는af(100, 5)의 결과이다.
출력 결과는 같지만, 기존 함수 표현식은 익명 함수와return을 사용하고, 화살표 함수는=>와 자동 반환을 사용한다.
화면에 적힌 화살표 함수 기본 형태
// exam1.html <pre> () => expression param => expression (param) => expression (param1, paramN) => expression () => { statements } param => { statements } (param1, paramN) => { statements } </pre>이 부분은 화살표 함수의 여러 작성 형태를 보여 준다.
화살표 함수는 매개변수가 몇 개인지, 함수 안 코드가 한 줄인지 여러 줄인지에 따라 모양이 조금 달라진다.
()는 매개변수가 없다는 뜻이다.
받을 값이 없을 때는 빈 괄호를 써야 한다.
param은 매개변수가 하나인 경우이다.
매개변수가 단순한 이름 하나라면 괄호를 생략할 수 있다.
(param1, paramN)은 매개변수가 여러 개인 경우이다.
매개변수가 여러 개이면 괄호를 반드시 사용해야 한다.
그리고 오른쪽에expression만 있는 형태는 결과가 자동으로 반환된다.
반면{ statements }처럼 중괄호를 사용하면 여러 줄 코드를 작성할 수 있지만, 결과를 반환하려면return을 직접 써야 한다.
화살표 함수는 모양이 여러 개처럼 보이지만 기준은 단순하다.
매개변수 개수와 중괄호 사용 여부만 구분하면 된다.
중괄호를 쓰는 화살표 함수와 주의할 점
화살표 함수도 중괄호를 사용할 수 있다.
다만 중괄호를 사용하면 자동 반환이 되지 않는다.
이때는return을 직접 써야 한다.// exam1.html // 비교용 예시 const af = (a, b) => { // 중괄호를 사용하면 함수 몸체가 블록이 된다. return a + b; // 블록 안에서는 return을 직접 써야 한다. }; console.log(af(100, 5)); // 105 출력이 코드는 원래 예제의
const af = (a, b) => a + b;와 같은 결과를 만든다.
하지만 중괄호를 사용했기 때문에return을 직접 작성했다.
반대로 아래처럼 쓰면 결과가 나오지 않는다.// exam1.html // 비교용 예시 const af = (a, b) => { // a + b를 계산만 하고 반환하지 않는다. a + b; }; console.log(af(100, 5)); // 반환값이 없어서 undefined 출력// 출력결과 // undefined이 코드는
a + b를 계산하기는 한다.
하지만return이 없기 때문에 계산 결과를 함수 밖으로 돌려주지 않는다.
그래서 함수 호출 결과는undefined가 된다.
화살표 함수에서 중괄호를 쓰면 반드시return을 직접 작성해야 결과가 함수 밖으로 나온다.
this는 현재 동작의 기준 객체를 가리킨다
예제 화면에는 “화살표 함수는 생성자로 사용할 수 없음”이라는 문장이 있다.
그 이유로 “자신만의this가 없기 때문”이라고 적혀 있다.
이 문장을 이해하려면 먼저this가 무엇인지 알아야 한다.
this는 함수나 메서드가 실행될 때, 지금 이 코드가 어떤 객체를 기준으로 실행되고 있는지 가리키는 값이다.
쉽게 말하면this는 “현재 이 동작의 주인 객체”를 가리키는 이름이라고 이해하면 된다.
예를 들어 객체 안에name값이 있고, 그 객체의 메서드에서this.name을 사용하면 “현재 이 메서드를 실행한 객체의name값”을 읽는다는 뜻이다.// exam1.html // 비교용 예시 const user = { name: "둘리", // 객체가 가진 값 hello: function () { // this는 이 메서드를 호출한 user 객체를 가리킨다. console.log(this.name); } }; user.hello(); // user 객체가 hello를 호출한다.// 출력결과 // 둘리이 코드에서
user.hello()를 실행했다.
hello()는user객체를 통해 호출되었다.
그래서hello()안의this는user객체를 가리킨다.
따라서this.name은user.name과 같은 값인"둘리"가 된다.
즉,this는 아무 값이나 가리키는 문법이 아니다.
함수가 어떤 방식으로 호출되었는지에 따라 기준 객체가 달라질 수 있는 값이다.
그래서this는 초보자가 헷갈리기 쉬운 개념이다.
화살표 함수는 자기만의 this를 만들지 않는다
화살표 함수는 일반 함수처럼 자기만의
this를 새로 만들지 않는다.
그래서 화살표 함수 안에서this를 사용하면, 화살표 함수 자신을 호출한 객체를 기준으로 새로 정해지는 것이 아니라 바깥쪽의this를 그대로 사용하려고 한다.
이 특징 때문에 화살표 함수는 짧은 계산 함수나callback함수에는 편하다.
하지만 객체 자기 자신의 값을this로 읽어야 하는 메서드에서는 조심해야 한다.// exam1.html // 비교용 예시 const user = { name: "둘리", // 객체가 가진 값 hello: () => { // 화살표 함수는 자기만의 this를 만들지 않는다. console.log(this.name); } }; user.hello(); // user 객체로 호출해도 this가 user로 새로 잡히지 않는다.이 코드는 초보자가 보기에는
this.name이"둘리"를 출력할 것처럼 보일 수 있다.
하지만 화살표 함수는 자기만의this를 만들지 않기 때문에 일반 메서드처럼 동작하지 않는다.
그래서 객체의 메서드에서 객체 자기 자신의 값을 읽어야 한다면, 화살표 함수보다 일반 함수 형태를 사용하는 것이 더 안전하다.
화살표 함수는 자기만의this를 만들지 않으므로, 객체 자기 자신의 값을this로 읽어야 하는 메서드에서는 주의해야 한다.
화살표 함수에서 주의할 점
예제 화면에는 화살표 함수의 주의점이 두 가지 적혀 있다.
첫 번째는 클래스의 메서드로 사용하는 것은 가능하다는 점이다.
두 번째는 생성자로 사용할 수 없다는 점이다.
여기서 “클래스의 메서드로 사용하는 것은 가능”하다는 말은 조심해서 이해해야 한다.
화살표 함수를 클래스 안에서 사용할 수는 있다.
하지만 화살표 함수는 자기만의this를 만들지 않으므로, 일반 메서드처럼 항상 같은 방식으로 이해하면 안 된다.
특히 객체 자기 자신을this로 다뤄야 하는 메서드에서는 일반 메서드 문법과 화살표 함수의 차이를 구분해야 한다.
생성자는new와 함께 사용해서 새 객체를 만드는 함수라고 이해하면 된다.
일반 함수는 상황에 따라new 함수이름()처럼 객체 생성용으로 사용할 수 있다.
하지만 화살표 함수는new로 호출되는 생성자 함수로 사용할 수 없다.
초보자 기준에서는 이렇게 이해하면 된다.
생성자 함수는 새 객체를 만들고, 그 새 객체를this로 다루는 흐름이 필요하다.
그런데 화살표 함수는 자기만의this를 만들지 않는다.
또new로 호출될 수 있는 생성자 기능도 가지지 않는다.
그래서 화살표 함수는 생성자로 사용할 수 없다.
정리하면 아래처럼 볼 수 있다.
- 간단한 계산 함수에는 화살표 함수가 잘 어울린다.
- 짧은
callback함수에도 화살표 함수가 자주 쓰인다.this는 현재 동작의 기준 객체를 가리키는 값이다.- 화살표 함수는 자기만의
this를 새로 만들지 않는다.- 클래스 안에서 화살표 함수를 사용할 수는 있지만,
this처리 방식은 일반 메서드와 다르다.- 객체 자기 자신을
this로 다뤄야 하는 메서드에서는 주의해서 사용해야 한다.- 화살표 함수는
new로 호출되는 생성자 함수로 사용할 수 없다.화살표 함수는 기존 함수를 전부 대체하는 문법이 아니라, 자기만의
this가 필요 없는 짧은 함수 표현식을 간결하게 작성하기 위한 문법이다.
핵심 정리
이 예제의 핵심은 기존 함수 표현식과 화살표 함수의 작성 방식 차이이다.
기존 함수 표현식은 이름 없는 익명 함수를 변수f에 저장한다.
화살표 함수는 같은 동작을=>를 사용해 더 짧게 작성한다.
이 흐름은 아래처럼 정리할 수 있다.
const f = function (a, b) { ... }는 익명 함수를 변수f에 저장한 함수 표현식이다.function (a, b)는 함수 이름이 없으므로 익명 함수이다.f는 함수 이름이라기보다 익명 함수가 저장된 변수 이름이다.const af = (a, b) => a + b는 같은 기능을 화살표 함수로 작성한 코드이다.- 기존 함수 표현식은
return을 직접 작성한다.- 화살표 함수는 중괄호 없이 표현식 하나를 쓰면 결과를 자동 반환한다.
f(100, 5)와af(100, 5)는 모두105를 출력한다.this는 현재 동작의 기준 객체를 가리키는 값이다.- 화살표 함수는 자기만의
this를 만들지 않는다.- 화살표 함수는
new로 호출되는 생성자 함수로 사용할 수 없다.- 클래스 안에서 화살표 함수를 사용할 수는 있지만, 일반 메서드와
this처리 방식이 같다고 보면 안 된다.따라서 이 예제가 알려 주는 핵심은 이것이다.
이 예제는 익명 함수를 변수에 저장한 기존 함수 표현식과 화살표 함수를 비교하면서, 화살표 함수가 간단한 함수 표현식을 더 짧고 읽기 쉽게 작성하게 해 주는 문법임을 보여 준다.
이 예제는
for...of반복문으로 배열과 문자열을 순서대로 꺼내는 흐름을 보여 준다.
반복문은 같은 동작을 여러 번 실행할 때 사용하는 문법이다.
그중for...of는 배열처럼 여러 값이 들어 있는 대상에서 값을 하나씩 꺼낼 때 사용한다.
이 예제에서는 먼저 배열coffeeNames에 들어 있는 커피 이름을 하나씩 꺼낸다.
그다음 문자열"hi!"에 들어 있는 문자를 하나씩 꺼낸다.
for...of의 핵심은 반복 가능한 값에서 실제 값을 순서대로 하나씩 꺼낸다는 점이다.
예제 전체 코드
// exam2.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>ECMAScript6</title> </head> <body> <h1>for~of 반복문(1)</h1> <hr /> <script> // 배열에 커피 이름 여러 개를 저장한다. let coffeeNames = ["Americano", "Cappuccino", "Espresso"]; // 배열에서 값을 하나씩 꺼내 name 변수에 담는다. for (let name of coffeeNames) { // 현재 꺼낸 커피 이름을 콘솔에 출력한다. console.log(name); } // 문자열을 변수에 저장한다. let str = "hi!"; // 문자열에서 문자를 하나씩 꺼내 ch 변수에 담는다. for (let ch of str) { // 현재 꺼낸 문자를 콘솔에 출력한다. console.log(ch); } </script> </body> </html>이 코드는 크게 두 부분으로 나누어 볼 수 있다.
첫 번째는 배열을for...of로 반복하는 부분이다.
두 번째는 문자열을for...of로 반복하는 부분이다.
배열은 여러 값을 순서대로 담는 자료구조이다.
coffeeNames배열에는"Americano","Cappuccino","Espresso"가 순서대로 들어 있다.
for...of는 이 배열 안의 값을 앞에서부터 하나씩 꺼낸다.
문자열도 문자들이 순서대로 모여 있는 값이다.
그래서"hi!"문자열도for...of로 반복할 수 있다.
이때 문자열 전체가 한 번에 나오는 것이 아니라"h","i","!"가 하나씩 나온다.
for...of는 배열뿐 아니라 문자열처럼 순서대로 꺼낼 수 있는 값에도 사용할 수 있다.
이 예제에서 확인할 핵심
for...of는 반복 가능한 대상에서 값을 하나씩 꺼낸다.- 배열에
for...of를 사용하면 배열 요소가 하나씩 나온다.- 문자열에
for...of를 사용하면 문자가 하나씩 나온다.let name은 반복할 때마다 현재 꺼낸 배열 값을 담는 변수이다.let ch는 반복할 때마다 현재 꺼낸 문자를 담는 변수이다.console.log()는 현재 꺼낸 값을 콘솔에 출력한다.이 예제는
for...of가 반복 횟수보다 값 자체에 집중하는 반복문이라는 점을 보여 준다.
일반적인for반복문처럼 인덱스를 직접 다루지 않고, 값이 순서대로 하나씩 나온다는 흐름을 확인하면 된다.
배열을 for of로 반복하는 코드
// exam2.html // 배열에 커피 이름 여러 개를 저장한다. let coffeeNames = ["Americano", "Cappuccino", "Espresso"]; // 배열에서 값을 하나씩 꺼내 name 변수에 담는다. for (let name of coffeeNames) { // 현재 꺼낸 커피 이름을 콘솔에 출력한다. console.log(name); }이 코드는 배열
coffeeNames에 들어 있는 값을 하나씩 출력한다.
배열은 여러 값을 순서대로 담는 구조이다.
여기서는 커피 이름 세 개가 배열 안에 들어 있다.
for (let name of coffeeNames)는coffeeNames에서 값을 하나씩 꺼내서name이라는 변수에 담겠다는 뜻이다.
반복이 한 번 실행될 때마다name에는 배열의 다음 값이 들어간다.
흐름은 아래처럼 볼 수 있다.
- 첫 번째 반복에서
name은"Americano"가 된다.- 두 번째 반복에서
name은"Cappuccino"가 된다.- 세 번째 반복에서
name은"Espresso"가 된다.그래서
console.log(name)은 커피 이름을 순서대로 출력한다.
배열에서for...of를 사용하면 배열 안의 요소가 순서대로 하나씩 변수에 담긴다.
배열 반복 출력 결과
// 출력결과 // Americano // Cappuccino // Espresso이 출력 결과는 배열에 들어 있던 값의 순서와 같다.
coffeeNames배열의 첫 번째 값은"Americano"이다.
그래서 가장 먼저"Americano"가 출력된다.
그다음 배열의 두 번째 값인"Cappuccino"가 출력된다.
마지막으로 세 번째 값인"Espresso"가 출력된다.
for...of는 배열의 값을 앞에서부터 하나씩 꺼내기 때문에 출력 순서도 배열에 저장된 순서와 같다.
문자열을 for of로 반복하는 코드
// exam2.html // 문자열을 변수에 저장한다. let str = "hi!"; // 문자열에서 문자를 하나씩 꺼내 ch 변수에 담는다. for (let ch of str) { // 현재 꺼낸 문자를 콘솔에 출력한다. console.log(ch); }이 코드는 문자열
"hi!"를for...of로 반복한다.
문자열은 글자들이 순서대로 모여 있는 값이다.
그래서for...of를 사용하면 문자열 안의 문자를 앞에서부터 하나씩 꺼낼 수 있다.
for (let ch of str)에서ch는 현재 꺼낸 문자를 담는 변수이다.
반복이 한 번 실행될 때마다ch에는 문자열의 다음 문자가 들어간다.
흐름은 아래처럼 볼 수 있다.
- 첫 번째 반복에서
ch는"h"가 된다.- 두 번째 반복에서
ch는"i"가 된다.- 세 번째 반복에서
ch는"!"가 된다.여기서 중요한 점은
"hi!"전체가 한 번에 출력되는 것이 아니라는 점이다.
for...of는 문자열도 순서대로 나누어 문자 하나씩 꺼낸다.
문자열에for...of를 사용하면 문자열 전체가 아니라 문자 하나하나가 순서대로 나온다.
문자열 반복 출력 결과
// 출력결과 // h // i // !이 출력 결과는 문자열
"hi!"를 문자 단위로 꺼낸 결과이다.
첫 번째 문자인"h"가 먼저 출력된다.
그다음"i"가 출력된다.
마지막으로"!"가 출력된다.
문자열도 순서를 가진 값이기 때문에for...of로 반복하면 문자 단위로 처리할 수 있다.
for of가 일반 for문보다 쉬운 지점
for...of는 값을 직접 꺼내는 반복문이다.
그래서 배열의 몇 번째 값인지 나타내는 인덱스를 직접 사용할 필요가 없다.
일반적인for반복문은 보통i같은 숫자 변수를 사용해서 배열의 위치를 직접 다룬다.
하지만for...of는 위치보다 값 자체를 바로 꺼내는 데 집중한다.
예를 들어 이 예제에서는 커피 이름의 위치 번호가 필요하지 않다.
필요한 것은 배열 안에 들어 있는 커피 이름 자체이다.
그래서for...of를 사용하면 코드가 더 단순해진다.
정리하면 아래처럼 구분할 수 있다.
- 값만 순서대로 꺼내고 싶으면
for...of를 사용하면 이해하기 쉽다.- 몇 번째 값인지 위치 번호가 필요하면 일반
for반복문이 필요할 수 있다.for...of는 배열이나 문자열처럼 순서대로 값을 꺼낼 수 있는 대상에 잘 어울린다.
for...of는 반복 가능한 대상에서 값 자체를 하나씩 꺼내고 싶을 때 사용하는 반복문이다.
핵심 정리
이 예제의 핵심은
for...of반복문이 값을 꺼내는 방식이다.
배열에서는 배열 요소가 하나씩 나오고, 문자열에서는 문자가 하나씩 나온다.
이 흐름은 아래처럼 정리할 수 있다.
coffeeNames는 커피 이름 여러 개를 담은 배열이다.for (let name of coffeeNames)는 배열 값을 하나씩 꺼내name에 담는다.name에는"Americano","Cappuccino","Espresso"가 순서대로 들어간다.str은"hi!"문자열이다.for (let ch of str)는 문자열의 문자를 하나씩 꺼내ch에 담는다.ch에는"h","i","!"가 순서대로 들어간다.for...of는 인덱스보다 값 자체를 바로 꺼내는 데 적합하다.따라서 이 예제가 알려 주는 핵심은 이것이다.
for...of반복문은 배열이나 문자열처럼 반복 가능한 값에서 실제 값을 순서대로 하나씩 꺼내기 위한 문법이다.
이 예제는
Map객체에 데이터를 저장하고, 저장된 데이터를 다시 꺼내고, 존재 여부를 확인하고, 삭제하는 흐름을 보여 준다.
Map은 여러 데이터를key와value한 쌍으로 저장하는 객체이다.
여기서key는 값을 찾기 위한 이름이고,value는 그 이름에 연결된 실제 값이다.
쉽게 말하면Map은 “이 이름으로 이 값을 저장해 둘게”라는 구조이다.
예를 들어"Apple"이라는 이름에"red"라는 값을 저장하면, 나중에"Apple"이라는 이름으로"red"를 다시 꺼낼 수 있다.
Map은 값을 그냥 순서대로 꺼내기 위한 배열이 아니라,key를 기준으로value를 저장하고 찾기 위한 자료구조이다.
Map도 데이터를 넣은 순서를 기억한다.
하지만 배열처럼0,1,2같은 위치 번호를 중심으로 값을 꺼내기보다,key를 사용해 연결된value를 찾는 흐름이 더 중요하다.
exam3.html에서는Map을 만들고set(),get(),has(),delete()를 사용하는 기본 흐름을 확인한다.
exam4.html에서는keys(),values(),entries(),forEach()를 사용해서Map안의 데이터를 반복해서 꺼내는 흐름을 확인한다.
Map이란 무엇인가
Map은JavaScript에서 여러 값을 저장할 때 사용하는 객체이다.
그런데 배열처럼 값만 저장하는 방식이 아니다.
Map은 값을 저장할 때key와value를 한 쌍으로 저장한다.
여기서key는 값을 찾기 위한 이름이다.
value는 실제로 저장되는 값이다.
예를 들어 아래처럼 생각하면 된다.
Apple이라는key에는red라는value가 연결된다.Melon이라는key에는green이라는value가 연결된다.즉,
Map은 아래처럼 데이터를 기억한다.// exam3.html // 이해를 위한 구조 예시이다. // Apple -> red // Melon -> green이 구조에서 중요한 것은
"red"를 꺼내기 위해"Apple"이라는key를 사용한다는 점이다.
배열처럼0번,1번 위치를 직접 지정해서 꺼내는 흐름과 다르다.
배열은 위치 순서를 기준으로 값을 꺼내는 경우가 많고,Map은 이름표 역할을 하는key를 기준으로 값을 꺼낸다.
예를 들어 커피 메뉴 이름만 순서대로 출력하고 싶다면 배열이 자연스럽다.
하지만 특정 이름에 연결된 값을 바로 찾고 싶다면Map이 더 자연스럽다.
Map은 특정key로 특정value를 저장하고 다시 꺼내기 위한 구조이다.
Map이 필요한 이유
Map이 필요한 이유는 데이터를 이름과 값의 쌍으로 관리해야 하는 경우가 많기 때문이다.
단순히 값만 나열하면 “이 값이 무엇을 의미하는지”를 코드에서 바로 알기 어렵다.
예를 들어"red","green"만 저장되어 있으면 이 값들이 어떤 과일의 색인지 바로 알기 어렵다.
하지만"Apple" → "red","Melon" → "green"처럼 저장하면 의미가 분명해진다.
Map을 사용하면 아래 작업을 전용 메서드로 처리할 수 있다.
set()으로 데이터를 저장한다.get()으로key에 연결된 값을 꺼낸다.has()로 특정key가 있는지 확인한다.delete()로 특정key와 연결된 값을 삭제한다.keys()로key만 꺼낸다.values()로value만 꺼낸다.entries()로key와value를 같이 꺼낸다.
Map은 데이터를 저장하는 것에서 끝나지 않고, 저장된 데이터를 조회·확인·삭제·반복하기 쉽게 만들어 준다.
예제 전체 코드 1
// exam3.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>ECMAScript6</title> </head> <body> <h1>Map 객체(1)</h1> <hr /> <script> // Apple은 key, red는 value이다. // Melon은 key, green은 value이다. let map = new Map([ ["Apple", "red"], ["Melon", "green"] ]); // Map 안에 저장된 key-value 쌍의 개수를 출력한다. console.log(map.size); // 현재 Map 객체 전체를 출력한다. console.log(map); // 빈 Map 객체를 새로 만든다. map = new Map(); // Apple이라는 key에 red라는 value를 저장한다. map.set("Apple", "red"); // Melon이라는 key에 green이라는 value를 저장한다. map.set("Melon", "green"); // Apple key에 연결된 value를 꺼낸다. console.log(map.get("Apple")); // Melon key에 연결된 value를 꺼낸다. console.log(map.get("Melon")); // Apple key가 Map 안에 있는지 확인한다. console.log(map.has("Apple")); // Melon key가 Map 안에 있는지 확인한다. console.log(map.has("Melon")); // Apple key와 그에 연결된 value를 삭제한다. map.delete("Apple"); // Apple key가 아직 남아 있는지 다시 확인한다. console.log(map.has("Apple")); </script> </body> </html>이 코드는
Map을 두 가지 방식으로 만든다.
첫 번째는new Map([["Apple", "red"], ["Melon", "green"]])처럼 처음부터 데이터를 넣어 만드는 방식이다.
두 번째는new Map()으로 빈Map을 만든 뒤set()으로 데이터를 하나씩 추가하는 방식이다.
이 예제에서"Apple"과"Melon"은key이다.
"red"와"green"은 각각의key에 연결된value이다.
즉,"Apple"이라는 이름으로"red"를 찾을 수 있고,"Melon"이라는 이름으로"green"을 찾을 수 있다.
Map은 데이터를 그냥 나열하는 배열이 아니라,key를 기준으로value를 저장하고 찾는 구조이다.
예제 전체 코드 2
// exam4.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>ECMAScript6</title> </head> <body> <h1>Map 객체(2)</h1> <hr /> <script> // key와 value가 들어 있는 Map 객체를 만든다. let map = new Map([ ["Apple", "red"], ["Melon", "green"] ]); // Map의 key들만 꺼낼 수 있는 반복자를 만든다. var keyIter = map.keys(); // key를 하나씩 꺼내 출력한다. for (var key of keyIter) { console.log(key); } // Map의 value들만 꺼낼 수 있는 반복자를 만든다. var valueIter = map.values(); // value를 하나씩 꺼내 출력한다. for (var value of valueIter) { console.log(value); } // Map의 key와 value 쌍을 꺼낼 수 있는 반복자를 만든다. // 원본 변수명은 entriyIter이지만, 선언과 사용이 같아서 실행은 된다. var entriyIter = map.entries(); // entry를 하나씩 꺼내 출력한다. for (var entry of entriyIter) { console.log(entry); } // Map의 데이터를 하나씩 돌면서 value, key, map 전체를 사용한다. map.forEach(function (value, key, map) { console.log(key + " : " + value + " : " + map); }); </script> </body> </html>이 코드는
Map안의 데이터를 반복해서 꺼내는 방법을 보여 준다.
keys()는key만 꺼낼 때 사용한다.
values()는value만 꺼낼 때 사용한다.
entries()는key와value를 한 쌍으로 꺼낼 때 사용한다.
원본 코드에는entriyIter라는 변수명이 나온다.
entryIter의 오타처럼 보이지만, 선언할 때와 사용할 때 같은 이름을 사용하고 있으므로 코드 실행 자체는 가능하다.
다만 의미상으로는entry를 반복하기 위한 변수이므로, 개념을 이해할 때는entry반복자라고 보면 된다.
마지막의forEach()는Map안의 데이터를 하나씩 순회하면서callback함수를 실행한다.
callback함수는 다른 함수의 인자로 전달되어 나중에 실행되는 함수이다.
여기서는function (value, key, map)이callback함수이다.
Map은 저장만 하는 객체가 아니라, 저장된key,value,entry를 목적에 맞게 꺼내 반복할 수 있는 객체이다.
이 예제에서 확인할 핵심
Map은key와value를 한 쌍으로 저장한다.key는 값을 찾기 위한 이름이다.value는key에 연결된 실제 값이다.new Map()은 빈Map객체를 만든다.new Map([[key, value]])형태로 처음부터 값을 넣어 만들 수도 있다.set()은Map에 데이터를 저장한다.get()은key에 연결된value를 꺼낸다.has()는 특정key가 있는지 확인한다.delete()는 특정key와 연결된 데이터를 삭제한다.size는Map에 저장된 데이터 쌍의 개수를 알려 준다.keys()는key만 꺼낸다.values()는value만 꺼낸다.entries()는key와value쌍을 꺼낸다.forEach()는Map안의 데이터를 하나씩 반복 처리한다.이 예제는
Map을 단순히 객체처럼 보관하는 용도가 아니라, 데이터를 저장하고 조회하고 반복하는 전용 구조로 이해하게 해 주는 예제이다.
Map 객체를 처음부터 데이터와 함께 만들기
// exam3.html // Apple은 key, red는 value이다. // Melon은 key, green은 value이다. let map = new Map([ ["Apple", "red"], ["Melon", "green"] ]); console.log(map.size); // Map 안의 key-value 쌍 개수를 출력한다. console.log(map); // Map 객체 전체를 출력한다.
new Map()은 새로운Map객체를 만든다.
여기서는 괄호 안에 배열을 넣었다.
이 배열 안에는 다시 작은 배열들이 들어 있다.
["Apple", "red"]는 하나의 데이터 쌍이다.
첫 번째 값"Apple"은key이고, 두 번째 값"red"는value이다.
즉,"Apple"이라는 이름에"red"라는 값이 연결된다.
["Melon", "green"]도 같은 구조이다.
"Melon"은key이고,"green"은value이다.
map.size는Map안에 저장된 데이터 쌍의 개수를 알려 준다.
현재는"Apple" → "red","Melon" → "green"두 쌍이 있으므로2가 출력된다.
출력 흐름은 아래처럼 볼 수 있다.// 출력결과 // 2 // Map(2) { 'Apple' => 'red', 'Melon' => 'green' }
Map객체 전체를 출력하는 모양은 실행 환경에 따라 조금 다르게 보일 수 있다.
하지만 핵심은Apple이red와 연결되어 있고,Melon이green과 연결되어 있다는 점이다.
Map에서 중요한 것은 값 하나만 저장하는 것이 아니라, 값을 찾을 기준인key와 실제 값인value를 함께 저장한다는 점이다.
빈 Map을 만들고 set으로 데이터 추가하기
// exam3.html // 빈 Map 객체를 새로 만든다. map = new Map(); // Apple이라는 key에 red라는 value를 저장한다. map.set("Apple", "red"); // Melon이라는 key에 green이라는 value를 저장한다. map.set("Melon", "green");이번에는 처음부터 데이터를 넣지 않고 빈
Map을 먼저 만들었다.
map = new Map()은 비어 있는Map을 새로 만드는 코드이다.
그다음set()을 사용해 데이터를 추가한다.
map.set("Apple", "red")는"Apple"이라는key에"red"라는value를 저장한다.
map.set("Melon", "green")는"Melon"이라는key에"green"이라는value를 저장한다.
set()의 구조는 아래처럼 이해하면 된다.
- 첫 번째 값은
key이다.- 두 번째 값은
value이다.key는 나중에 값을 찾을 때 사용하는 이름이다.value는 실제로 저장되는 값이다.
set()은Map에 새로운key-value데이터를 저장하는 메서드이다.
get으로 값 꺼내기
// exam3.html // Apple key에 연결된 value를 꺼낸다. console.log(map.get("Apple")); // Melon key에 연결된 value를 꺼낸다. console.log(map.get("Melon"));
get()은key를 사용해서value를 꺼내는 메서드이다.
map.get("Apple")은"Apple"이라는key에 연결된 값을 찾는다.
그래서"red"가 나온다.
map.get("Melon")은"Melon"이라는key에 연결된 값을 찾는다.
그래서"green"이 나온다.
출력 결과는 아래와 같다.// 출력결과 // red // green여기서 중요한 점은
Map이 값을 순서 번호로 찾는 것이 아니라는 점이다.
배열은 보통0,1,2같은 위치 번호로 값을 꺼낸다.
반면Map은key를 기준으로 값을 꺼낸다.
Map에서get()은 위치 번호가 아니라key를 사용해서 값을 꺼낸다.
has로 존재 여부 확인하기
// exam3.html // Apple key가 Map 안에 있는지 확인한다. console.log(map.has("Apple")); // Melon key가 Map 안에 있는지 확인한다. console.log(map.has("Melon"));
has()는 특정key가Map안에 있는지 확인하는 메서드이다.
있으면true를 반환하고, 없으면false를 반환한다.
현재Map에는"Apple"과"Melon"이 모두 들어 있다.
그래서 두 결과 모두true가 나온다.// 출력결과 // true // true
true는 참이라는 뜻이다.
즉, 해당key가 존재한다는 의미이다.
has()는 값을 꺼내기 전에 해당key가 있는지 먼저 확인할 때 사용할 수 있다.
delete로 데이터 삭제하기
// exam3.html // Apple key와 그에 연결된 value를 삭제한다. map.delete("Apple"); // Apple key가 아직 남아 있는지 다시 확인한다. console.log(map.has("Apple"));
delete()는 특정key와 그key에 연결된value를 삭제한다.
map.delete("Apple")을 실행하면"Apple" → "red"데이터 쌍이Map에서 사라진다.
그다음map.has("Apple")을 다시 실행한다.
이미"Apple"을 삭제했기 때문에 결과는false이다.// 출력결과 // false
false는 거짓이라는 뜻이다.
즉,"Apple"이라는key가 더 이상Map안에 없다는 의미이다.
delete()는key만 지우는 것이 아니라, 그key에 연결된value까지 함께 삭제한다.
keys로 key만 반복하기
// exam4.html // key와 value가 들어 있는 Map 객체를 만든다. let map = new Map([ ["Apple", "red"], ["Melon", "green"] ]); // Map의 key들만 꺼낼 수 있는 반복자를 만든다. var keyIter = map.keys(); // key를 하나씩 꺼내 출력한다. for (var key of keyIter) { console.log(key); }
keys()는Map에 들어 있는key만 꺼낼 수 있게 해 준다.
여기서는"Apple"과"Melon"이key이다.
map.keys()의 결과는 바로 배열처럼 보일 수 있지만, 정확히는 반복할 수 있는 객체이다.
초보자 단계에서는 “key를 하나씩 꺼낼 준비가 된 묶음”이라고 이해하면 된다.
for...of는 이 반복 가능한 값에서key를 하나씩 꺼낸다.
그래서 출력 결과는 아래와 같다.// 출력결과 // Apple // Melon
keys()는Map에서 값이 아니라 값을 찾는 이름인key만 꺼낼 때 사용한다.
values로 value만 반복하기
// exam4.html // Map의 value들만 꺼낼 수 있는 반복자를 만든다. var valueIter = map.values(); // value를 하나씩 꺼내 출력한다. for (var value of valueIter) { console.log(value); }
values()는Map에 들어 있는value만 꺼낼 수 있게 해 준다.
여기서는"red"와"green"이value이다.
for...of로valueIter를 반복하면value가 하나씩 나온다.
그래서 출력 결과는 아래와 같다.// 출력결과 // red // green
keys()가 이름만 꺼내는 메서드라면,values()는 실제 값만 꺼내는 메서드이다.
key가 필요 없고 저장된 값만 보고 싶을 때는values()를 사용하면 된다.
entries로 key와 value를 함께 반복하기
// exam4.html // Map의 key와 value 쌍을 꺼낼 수 있는 반복자를 만든다. // 원본 변수명은 entriyIter이지만, 선언과 사용이 같아서 실행은 된다. var entriyIter = map.entries(); // entry를 하나씩 꺼내 출력한다. for (var entry of entriyIter) { console.log(entry); }
entries()는key와value를 한 쌍으로 꺼낸다.
여기서 한 쌍으로 꺼낸 값을entry라고 보면 된다.
현재Map에는 두 쌍의 데이터가 있다.
첫 번째entry는["Apple", "red"]이다.
두 번째entry는["Melon", "green"]이다.
코드의 변수명은entriyIter이다.
entryIter라고 쓰는 것이 더 자연스럽지만, 원본 코드에서는entriyIter로 선언하고entriyIter로 그대로 사용한다.
그래서 변수 이름이 일치하므로 실행에는 문제가 없다.
다만 개념적으로는entry를 꺼내는 반복자라고 이해하면 된다.
출력 결과는 아래와 비슷하게 보인다.// 출력결과 // ['Apple', 'red'] // ['Melon', 'green']브라우저 콘솔에서는 배열 모양이 조금 다르게 표시될 수 있다.
하지만 핵심은key와value가 함께 나온다는 점이다.
entries()는Map에서key와value를 같이 봐야 할 때 사용한다.
forEach로 Map 전체 반복하기
// exam4.html // Map의 데이터를 하나씩 돌면서 value, key, map 전체를 사용한다. map.forEach(function (value, key, map) { console.log(key + " : " + value + " : " + map); });
forEach()는Map안의 데이터를 하나씩 반복 처리하는 메서드이다.
여기서는function (value, key, map) { ... }가 반복할 때마다 실행된다.
이 함수의 첫 번째 매개변수value에는 현재 데이터의 값이 들어간다.
두 번째 매개변수key에는 현재 데이터의 이름이 들어간다.
세 번째 매개변수map에는 현재 반복 중인Map객체 전체가 들어간다.
주의할 점은 순서이다.
Map의forEach()에서는callback함수의 매개변수 순서가value,key,map이다.
일반적으로key-value라고 말하니까key가 먼저 올 것 같지만, 이 메서드에서는value가 먼저 들어온다.
출력 결과는 아래처럼 볼 수 있다.// 출력결과 // Apple : red : [object Map] // Melon : green : [object Map]여기서
[object Map]은Map객체 전체가 문자열로 바뀌어 출력된 모습이다.
중요한 것은 앞의Apple : red,Melon : green흐름이다.
Map의forEach()콜백 함수는value,key,map순서로 값을 받는다.
이 순서를 헷갈리면key와value를 반대로 이해할 수 있다.
Map과 일반 객체는 비슷하지만 다르다
Map은key와value를 저장한다는 점에서 일반 객체와 비슷해 보일 수 있다.
일반 객체도 프로퍼티 이름과 값을 묶어 저장하기 때문이다.
하지만 이 예제에서는Map의 전용 메서드를 사용하는 흐름이 중요하다.
Map은set(),get(),has(),delete()처럼 데이터를 다루는 메서드가 분명하게 준비되어 있다.
또keys(),values(),entries()로 원하는 부분만 꺼내 반복할 수 있다.
초보자 단계에서는 이렇게 구분하면 된다.
- 일반 객체는 프로퍼티를 중심으로 값을 묶어 표현할 때 자주 사용한다.
Map은key와value쌍을 추가, 조회, 삭제, 반복하는 흐름이 분명할 때 사용한다.Map은 저장된 데이터 개수를size로 바로 확인할 수 있다.Map은 반복 관련 메서드가 잘 준비되어 있다.
Map은key-value데이터를 저장하고, 그 데이터를 추가·조회·삭제·반복하기 위해 사용하는 전용 객체이다.
핵심 정리
이 예제의 핵심은
Map객체의 기본 사용 흐름이다.
Map은key와value를 한 쌍으로 저장하고, 그key를 기준으로 값을 조회하거나 삭제할 수 있다.
이 흐름은 아래처럼 정리할 수 있다.
Map은key와value를 한 쌍으로 저장하는 객체이다.key는 값을 찾기 위한 이름이다.value는key에 연결된 실제 값이다.- 배열은 위치 순서 중심이고,
Map은key중심이다.Map도 저장 순서를 기억하지만, 값을 찾을 때는key를 사용하는 흐름이 중요하다.new Map()은 빈Map객체를 만든다.new Map([[key, value]])형태로 처음부터 데이터를 넣어 만들 수도 있다.size는 저장된 데이터 쌍의 개수를 알려 준다.set(key, value)는 새로운 데이터를 저장한다.get(key)는key에 연결된value를 꺼낸다.has(key)는 해당key가 있는지 확인한다.delete(key)는 해당key와 연결된value를 삭제한다.keys()는key만 반복해서 꺼낸다.values()는value만 반복해서 꺼낸다.entries()는key와value쌍을 반복해서 꺼낸다.forEach()는Map안의 데이터를 하나씩 반복 처리한다.Map의forEach()콜백 매개변수 순서는value,key,map이다.따라서 이 예제가 알려 주는 핵심은 이것이다.
Map은key와value를 한 쌍으로 저장하고, 전용 메서드를 통해 데이터를 추가하고 조회하고 삭제하고 반복할 수 있게 해 주는 객체이다.
이 예제는
Set객체에 값을 저장하고, 값이 있는지 확인하고, 값을 삭제하고, 반복해서 꺼내는 흐름을 보여 준다.
Set은 여러 값을 저장하는 객체이다.
다만 배열처럼 같은 값을 여러 번 저장하는 구조가 아니라, 중복 없이 값만 저장하는 구조이다.
앞에서 본Map은key와value를 한 쌍으로 저장했다.
하지만Set은key와value를 나누지 않는다.
값 자체를 저장하고, 그 값이 있는지 확인하는 데 집중한다.
Set의 핵심은 중복 없는 값 묶음을 만들고 관리하는 것이다.
Set이란 무엇인가
Set은JavaScript에서 여러 값을 하나의 묶음으로 저장할 때 사용하는 객체이다.
배열과 비슷하게 여러 값을 담을 수 있지만, 중요한 차이가 있다.
Set은 같은 값을 중복해서 저장하지 않는다.
예를 들어"Apple"을 이미 저장한 상태에서 다시"Apple"을 추가하려고 해도,Set안에는"Apple"이 하나만 유지된다.
즉,Set은 값의 목록을 만들되, 같은 값이 여러 번 들어가는 상황을 막고 싶을 때 사용할 수 있다.
또Set은 값을 넣은 순서를 기억한다.
그래서for...of로 반복하면 값이 추가된 순서대로 나온다.
다만 배열처럼 위치 번호를 중심으로 값을 다루기보다는, 값 자체가 있는지 없는지를 확인하는 흐름이 더 중요하다.
배열과Set은 이렇게 구분하면 된다.
- 배열은 값의 순서와 위치를 다룰 때 자주 사용한다.
Set은 중복 없는 값 묶음이 필요할 때 사용한다.- 배열은 같은 값을 여러 번 가질 수 있다.
Set은 같은 값을 하나만 가진다.Set은 값을 넣은 순서대로 반복할 수 있다.
Set은 값이 이미 있는지 없는지가 중요한 상황에서 사용하기 좋은 자료구조이다.
Set이 필요한 이유
Set이 필요한 이유는 중복을 신경 써야 하는 경우가 많기 때문이다.
예를 들어 사용자가 선택한 과일 목록을 저장한다고 생각해 보자.
이미"Apple"을 선택했는데 또"Apple"이 들어가면 같은 값이 반복된다.
이런 상황에서 배열만 사용하면 중복이 들어갔는지 직접 검사해야 한다.
하지만Set은 같은 값을 중복 저장하지 않는 구조이기 때문에, 값의 중복을 자연스럽게 막을 수 있다.
Set을 사용하면 아래 작업을 전용 메서드로 처리할 수 있다.
add()로 값을 추가한다.has()로 값이 있는지 확인한다.delete()로 값을 삭제한다.for...of로 값을 하나씩 꺼낸다.forEach()로 값을 반복 처리한다.
Set은 값을 저장하는 것에서 끝나지 않고, 값의 존재 확인과 삭제, 반복 처리를 쉽게 해 준다.
예제 전체 코드
// exam5.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>ECMAScript6</title> </head> <body> <h1>Set 객체</h1> <hr /> <script> // 비어 있는 Set 객체를 만든다. let fruitSet = new Set(); // 비어 있는 Set 객체를 출력한다. console.log(fruitSet); // Apple과 Melon 값을 가진 Set 객체를 새로 만든다. fruitSet = new Set(["Apple", "Melon"]); // 값이 들어 있는 Set 객체를 출력한다. console.log(fruitSet); // 다시 비어 있는 Set 객체를 만든다. fruitSet = new Set(); // Set에 Apple 값을 추가한다. fruitSet.add("Apple"); // Set에 Melon 값을 추가한다. fruitSet.add("Melon"); // 현재 Set 객체 전체를 출력한다. console.log(fruitSet); // Apple 값이 Set 안에 있는지 확인한다. console.log(fruitSet.has("Apple")); // Melon 값이 Set 안에 있는지 확인한다. console.log(fruitSet.has("Melon")); // Apple 값을 Set에서 삭제한다. fruitSet.delete("Apple"); // Apple 값이 아직 Set 안에 있는지 다시 확인한다. console.log(fruitSet.has("Apple")); // Apple 삭제 후 현재 Set 객체 전체를 출력한다. console.log(fruitSet); // Set 안의 값을 하나씩 꺼내 fruit 변수에 담는다. for (var fruit of fruitSet) { // 현재 꺼낸 값을 출력한다. console.log(fruit); } // Set 안의 값을 하나씩 반복 처리한다. fruitSet.forEach(function (value1, value2, set) { // Set은 key가 없어서 value1과 value2가 같은 값으로 들어온다. console.log(value1 + " : " + value2 + " : " + set); }); </script> </body> </html>이 코드는 크게 여섯 부분으로 나누어 볼 수 있다.
첫 번째는 비어 있는Set을 만드는 부분이다.
두 번째는 처음부터 값이 들어 있는Set을 만드는 부분이다.
세 번째는add()로 값을 추가하는 부분이다.
네 번째는has()로 값의 존재 여부를 확인하는 부분이다.
다섯 번째는delete()로 값을 삭제하는 부분이다.
여섯 번째는for...of와forEach()로Set의 값을 반복해서 꺼내는 부분이다.
이 예제에서 중요한 점은Set에는key가 없다는 것이다.
Map은"Apple"이라는key에"red"라는value를 연결했다.
하지만Set은"Apple"이라는 값 자체를 저장한다.
Set은key-value구조가 아니라 값 자체를 중복 없이 저장하는 구조이다.
이 예제에서 확인할 핵심
Set은 중복 없는 값 묶음을 저장한다.new Set()은 비어 있는Set객체를 만든다.new Set([값1, 값2])처럼 처음부터 값을 넣어 만들 수도 있다.add()는Set에 값을 추가한다.- 같은 값을 다시 추가해도
Set안에는 하나만 남는다.has()는 특정 값이Set안에 있는지 확인한다.delete()는 특정 값을Set에서 삭제한다.for...of는Set의 값을 하나씩 꺼낸다.forEach()는Set의 값을 하나씩 반복 처리한다.Set은Map처럼key와value를 나누지 않는다.이 예제는
Set을 값 목록처럼 사용하되, 중복 없이 관리하는 흐름을 보여 준다.
비어 있는 Set 만들기
// exam5.html // 비어 있는 Set 객체를 만든다. let fruitSet = new Set(); // 비어 있는 Set 객체를 출력한다. console.log(fruitSet);
new Set()은 새로운Set객체를 만든다.
괄호 안에 아무 값도 넣지 않았기 때문에 처음에는 비어 있는 상태이다.
fruitSet은 이Set객체를 저장하는 변수이다.
이제fruitSet을 통해 값을 추가하거나, 값이 있는지 확인하거나, 값을 삭제할 수 있다.
출력 결과는 아래처럼 볼 수 있다.// 출력결과 // Set(0) {}
Set(0)은Set안에 값이0개 있다는 뜻이다.
즉, 아직 아무 값도 저장되지 않은 상태이다.
비어 있는Set은 값을 담을 준비만 된 상태라고 이해하면 된다.
처음부터 값이 들어 있는 Set 만들기
// exam5.html // Apple과 Melon 값을 가진 Set 객체를 새로 만든다. fruitSet = new Set(["Apple", "Melon"]); // 값이 들어 있는 Set 객체를 출력한다. console.log(fruitSet);이번에는
new Set(["Apple", "Melon"])처럼 배열을 넣어서Set을 만들었다.
이렇게 하면 처음부터"Apple"과"Melon"이 들어 있는Set이 만들어진다.
여기서 배열은Set에 넣을 초기값을 전달하는 역할을 한다.
즉, 배열 자체를 저장하는 것이 아니라 배열 안의 값들이Set안으로 들어간다.
출력 결과는 아래처럼 볼 수 있다.// 출력결과 // Set(2) { 'Apple', 'Melon' }
Set(2)는Set안에 값이2개 있다는 뜻이다.
현재 값은"Apple"과"Melon"이다.
new Set([값1, 값2])형태를 사용하면 처음부터 값이 들어 있는Set을 만들 수 있다.
add로 값 추가하기
// exam5.html // 다시 비어 있는 Set 객체를 만든다. fruitSet = new Set(); // Set에 Apple 값을 추가한다. fruitSet.add("Apple"); // Set에 Melon 값을 추가한다. fruitSet.add("Melon"); // 현재 Set 객체 전체를 출력한다. console.log(fruitSet);이 부분에서는 다시 빈
Set을 만든 뒤add()로 값을 하나씩 추가한다.
fruitSet.add("Apple")은Set에"Apple"값을 추가한다.
fruitSet.add("Melon")은Set에"Melon"값을 추가한다.
Map에서는set(key, value)를 사용했다.
반면Set에서는add(value)를 사용한다.
왜냐하면Set에는key가 따로 없고 값 자체만 저장하기 때문이다.
출력 결과는 아래처럼 볼 수 있다.// 출력결과 // Set(2) { 'Apple', 'Melon' }
add()는Set에 값을 추가하는 메서드이다.
그리고 같은 값을 다시 추가하더라도Set은 중복 값을 여러 개 만들지 않는다.
Set은 같은 값을 중복 저장하지 않는다
원본 코드에는 같은 값을 두 번 추가하는 코드는 없다.
하지만Set을 이해하려면 중복 저장이 되지 않는다는 점을 꼭 알아야 한다.
아래 코드는 그 특징을 확인하기 위한 비교용 예시이다.// exam5.html // 비교용 예시이다. const fruitSet = new Set(); fruitSet.add("Apple"); // Apple 값을 추가한다. fruitSet.add("Apple"); // 같은 Apple 값을 다시 추가한다. fruitSet.add("Melon"); // Melon 값을 추가한다. console.log(fruitSet); // 중복된 Apple은 하나만 남는다.// 출력결과 // Set(2) { 'Apple', 'Melon' }
Apple을 두 번 추가했지만 결과에는"Apple"이 하나만 남는다.
이것이Set의 가장 중요한 특징이다.
배열이라면"Apple"을 두 번 넣었을 때 두 개가 그대로 들어갈 수 있다.
하지만Set은 같은 값을 중복해서 보관하지 않는다.
Set은 같은 값을 여러 번 추가해도 하나만 유지한다.
이 점 때문에 중복 없는 값 목록을 만들 때Set을 사용할 수 있다.
has로 값 존재 여부 확인하기
// exam5.html // Apple 값이 Set 안에 있는지 확인한다. console.log(fruitSet.has("Apple")); // Melon 값이 Set 안에 있는지 확인한다. console.log(fruitSet.has("Melon"));
has()는 특정 값이Set안에 있는지 확인하는 메서드이다.
값이 있으면true를 반환하고, 값이 없으면false를 반환한다.
현재fruitSet에는"Apple"과"Melon"이 모두 들어 있다.
그래서 두 결과 모두true가 나온다.// 출력결과 // true // true
true는 참이라는 뜻이다.
즉, 해당 값이Set안에 있다는 의미이다.
Map의has()는 특정key가 있는지 확인했다.
하지만Set의has()는 특정 값 자체가 있는지 확인한다.
Set의has()는key가 아니라 값 자체의 존재 여부를 확인한다.
delete로 값 삭제하기
// exam5.html // Apple 값을 Set에서 삭제한다. fruitSet.delete("Apple"); // Apple 값이 아직 Set 안에 있는지 다시 확인한다. console.log(fruitSet.has("Apple")); // Apple 삭제 후 현재 Set 객체 전체를 출력한다. console.log(fruitSet);
delete()는Set에서 특정 값을 삭제하는 메서드이다.
fruitSet.delete("Apple")을 실행하면"Apple"값이Set에서 사라진다.
그다음fruitSet.has("Apple")을 실행하면 결과는false이다.
이미"Apple"을 삭제했기 때문이다.
출력 결과는 아래처럼 볼 수 있다.// 출력결과 // false // Set(1) { 'Melon' }
Set(1)은 현재Set안에 값이 하나만 남았다는 뜻이다.
남은 값은"Melon"이다.
delete()는Set에서 값 자체를 삭제한다.
Map처럼key와 연결된 값을 함께 삭제하는 구조가 아니라, 저장된 값 하나를 제거하는 구조이다.
for of로 Set 값 반복하기
// exam5.html // Set 안의 값을 하나씩 꺼내 fruit 변수에 담는다. for (var fruit of fruitSet) { // 현재 꺼낸 값을 출력한다. console.log(fruit); }
for...of는 반복 가능한 대상에서 값을 하나씩 꺼내는 반복문이다.
Set도 반복 가능한 객체이기 때문에for...of를 사용할 수 있다.
이 시점의fruitSet에는"Apple"이 삭제되고"Melon"만 남아 있다.
그래서 반복해서 꺼낼 값도"Melon"하나뿐이다.
출력 결과는 아래와 같다.// 출력결과 // Melon
for (var fruit of fruitSet)에서fruit는 현재 꺼낸 값을 담는 변수이다.
첫 번째 반복에서fruit에는"Melon"이 들어간다.
더 꺼낼 값이 없으므로 반복은 끝난다.
Set에for...of를 사용하면Set안에 남아 있는 값을 하나씩 꺼낼 수 있다.
forEach로 Set 반복하기
// exam5.html // Set 안의 값을 하나씩 반복 처리한다. fruitSet.forEach(function (value1, value2, set) { // Set은 key가 없어서 value1과 value2가 같은 값으로 들어온다. console.log(value1 + " : " + value2 + " : " + set); });
forEach()는Set안의 값을 하나씩 반복 처리하는 메서드이다.
여기서는function (value1, value2, set) { ... }가 반복할 때마다 실행된다.
Set의forEach()는 매개변수 세 개를 받을 수 있다.
첫 번째 매개변수value1에는 현재 값이 들어간다.
두 번째 매개변수value2에도 현재 값이 들어간다.
세 번째 매개변수set에는 현재 반복 중인Set객체 전체가 들어간다.
여기서value1과value2가 같은 이유가 중요하다.
Map은key와value가 있기 때문에forEach(value, key, map)구조였다.
하지만Set은key가 없다.
그래서Set의forEach()에서는value1과value2가 같은 값으로 들어온다.
출력 결과는 아래처럼 볼 수 있다.// 출력결과 // Melon : Melon : [object Set]
[object Set]은Set객체 전체가 문자열로 바뀌어 출력된 모습이다.
중요한 것은 앞의Melon : Melon이다.
Set의forEach()는key가 없기 때문에 첫 번째 값과 두 번째 값이 같은 값으로 들어온다.
이 부분은Map의forEach()와 비교해서 반드시 구분해야 한다.
Set과 Map 비교하기
Set과Map은 둘 다 여러 데이터를 저장하는 객체이다.
하지만 저장 방식이 다르다.
Map은key와value를 한 쌍으로 저장한다.
그래서"Apple"이라는key에"red"라는value를 연결할 수 있다.
값을 꺼낼 때도map.get("Apple")처럼key를 사용한다.
반면Set은 값 자체만 저장한다.
fruitSet.add("Apple")처럼"Apple"이라는 값 자체를 넣는다.
값이 있는지 확인할 때도fruitSet.has("Apple")처럼 값 자체를 검사한다.
정리하면 아래처럼 볼 수 있다.
Map은key-value쌍을 저장한다.Set은 값 자체를 저장한다.Map은set(key, value)로 저장한다.Set은add(value)로 저장한다.Map의has()는key가 있는지 확인한다.Set의has()는 값이 있는지 확인한다.Map의forEach()는value,key,map순서로 값을 받는다.Set의forEach()는value,value,set처럼 같은 값이 두 번 들어온다.
Map은 이름표인key로 값을 찾는 구조이고,Set은 중복 없는 값 자체를 관리하는 구조이다.
핵심 정리
이 예제의 핵심은
Set이 값을 중복 없이 저장하고 관리하는 방식이다.
Set은key와value를 나누지 않는다.
값 자체를 저장하고, 그 값이 있는지 확인하고, 필요하면 삭제한다.
이 흐름은 아래처럼 정리할 수 있다.
Set은 중복 없는 값 묶음을 저장하는 객체이다.new Set()은 비어 있는Set객체를 만든다.new Set([값1, 값2])형태로 처음부터 값을 넣어 만들 수도 있다.add(value)는Set에 값을 추가한다.- 같은 값을 여러 번 추가해도
Set안에는 하나만 남는다.has(value)는 해당 값이Set안에 있는지 확인한다.delete(value)는 해당 값을Set에서 삭제한다.Set은key-value구조가 아니라 값 자체를 저장하는 구조이다.for...of를 사용하면Set안의 값을 하나씩 꺼낼 수 있다.Set의forEach()는value1,value2,set을 받을 수 있다.Set에는key가 없으므로value1과value2는 같은 값으로 들어온다.Map은key로 값을 찾는 구조이고,Set은 값 자체의 존재 여부를 관리하는 구조이다.따라서 이 예제가 알려 주는 핵심은 이것이다.
Set은 중복 없는 값 묶음을 만들고, 그 값이 있는지 확인하거나 삭제하고 반복해서 꺼낼 수 있게 해 주는 객체이다.
이 예제는 함수에 전달되는 인자 개수가 정해져 있지 않을 때 값을 어떻게 받는지 보여 준다.
또 함수 호출 시 인자를 생략했을 때 기본값을 어떻게 사용하는지도 함께 보여 준다.
여기서 먼저 구분해야 할 것은Rest와 기본값 매개변수이다.
Rest매개변수는 여러 개의 인자를 배열로 모아 받는 문법이다.
기본값 매개변수는 인자가 전달되지 않았을 때 미리 정해 둔 값을 대신 사용하는 문법이다.
이 예제의 핵심은 함수가 받는 값의 개수가 달라져도 유연하게 처리할 수 있다는 점이다.
Rest 매개변수란 무엇인가
Rest매개변수는 함수에 전달된 여러 값을 배열로 모아 받는 문법이다.
여기서Rest는 “나머지”라는 뜻으로 이해하면 된다.
예를 들어 함수가a,b를 먼저 받고, 그 뒤에 값이 더 들어올 수 있다고 생각해 보자.
이때...args를 사용하면 앞에서 받은 값을 제외한 나머지 값들이args배열에 들어간다.
아래처럼 이해하면 된다.
add(1, 2, 3, 4, 5)를 호출한다.a에는1이 들어간다.b에는2가 들어간다.- 나머지
3,4,5는args배열에 들어간다.즉,
...args는 값을 하나만 받는 자리가 아니다.
남은 값 여러 개를 배열 하나로 묶어 받는 자리이다.
Rest매개변수는 함수에 들어온 여러 인자 중 남은 값들을 배열로 모아 받는 문법이다.
기본값 매개변수란 무엇인가
기본값 매개변수는 함수 호출 시 값이 전달되지 않았을 때 사용할 값을 미리 정해 두는 문법이다.
예를 들어function add2(a, b = 10)이라고 작성하면,b값이 전달되지 않았을 때b에는 자동으로10이 들어간다.
그래서add2(1)처럼 하나의 값만 전달해도a는1,b는10이 된다.
반대로add2(100, 200)처럼 두 번째 값을 직접 전달하면 기본값10은 사용되지 않는다.
이때b에는 직접 전달된200이 들어간다.
기본값 매개변수는 값이 없을 때만 대신 사용되는 값이다.
값이 직접 전달되면 전달된 값이 우선한다.
예제 전체 코드
// exam6.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>ECMAScript6</title> </head> <body> <h1>Rest 연산자와 기본값 매개변수</h1> <hr /> <script> // 1. 앞의 두 값은 a, b로 받고 나머지 값은 args 배열로 받는다. function add(a, b, ...args) { // a, b, args에 각각 어떤 값이 들어왔는지 확인한다. console.log(a, b, args); // 먼저 a와 b를 더해서 합계를 시작한다. let sum = a + b; // args 배열에 들어 있는 나머지 숫자들을 하나씩 더한다. for (let i = 0; i < args.length; i++) { sum += args[i]; } // 최종 합계를 함수 밖으로 반환한다. return sum; } // add 함수에 숫자 5개를 전달한다. let sum = add(1, 2, 3, 4, 5); // add 함수가 반환한 합계를 출력한다. console.log(sum); // 2. 전달된 모든 값을 args 배열 하나로 받는다. function add1(...args) { // args에 어떤 값들이 배열로 들어왔는지 확인한다. console.log(args); // 합계를 0부터 시작한다. let sum = 0; // args 배열에 들어 있는 모든 숫자를 하나씩 더한다. for (let i = 0; i < args.length; i++) { sum += args[i]; } // 최종 합계를 함수 밖으로 반환한다. return sum; } // add1 함수에 숫자 5개를 전달한다. sum = add1(1, 2, 3, 4, 5); // add1 함수가 반환한 합계를 출력한다. console.log(sum); // 3. b에 기본값 10을 지정한다. function add2(a, b = 10) { // a에 들어온 값을 출력한다. console.log("a = " + a); // b에 들어온 값 또는 기본값을 출력한다. console.log("b = " + b); // a와 b를 더한 결과를 반환한다. return a + b; } // 두 번째 인자를 생략했으므로 b에는 기본값 10이 들어간다. sum = add2(1); // add2 함수가 반환한 합계를 출력한다. console.log(sum); // 두 번째 인자를 직접 전달했으므로 기본값 10은 사용되지 않는다. console.log(add2(100, 200)); </script> </body> </html>이 코드는 크게 세 부분으로 나누어 볼 수 있다.
첫 번째는a,b를 먼저 받고 나머지 값을args로 모으는add()함수이다.
두 번째는 전달된 모든 값을args하나로 모으는add1()함수이다.
세 번째는b = 10으로 기본값을 지정한add2()함수이다.
add()와add1()은 모두...args를 사용한다.
하지만add()는 앞의 두 값을 따로 받고 나머지만args에 모은다.
반면add1()은 전달된 모든 값을 처음부터args에 모은다.
add2()는Rest매개변수를 쓰지 않는다.
대신 두 번째 매개변수b에 기본값10을 지정한다.
그래서 두 번째 인자가 없을 때도 계산을 계속할 수 있다.
이 예제는 여러 인자를 배열로 모으는 방식과 생략된 인자에 기본값을 넣는 방식을 함께 비교한다.
이 예제에서 확인할 핵심
...args는 여러 인자를 배열로 모아 받는다.function add(a, b, ...args)는 앞의 두 값은 따로 받고, 나머지 값만args에 모은다.function add1(...args)는 전달된 모든 값을args배열에 모은다.args.length는args배열에 들어 있는 값의 개수를 뜻한다.for반복문은args배열의 값을 하나씩 꺼내 합계에 더한다.b = 10은b값이 전달되지 않았을 때 사용할 기본값이다.- 기본값은 인자가 생략되었을 때만 사용된다.
- 인자를 직접 전달하면 기본값보다 전달된 값이 우선한다.
이 예제는 함수가 고정된 개수의 값만 받는 것이 아니라, 여러 개의 값이나 생략된 값도 처리할 수 있다는 점을 보여 준다.
add 함수는 앞의 값과 나머지 값을 나누어 받는다
// exam6.html // 앞의 두 값은 a, b로 받고 나머지 값은 args 배열로 받는다. function add(a, b, ...args) { // a, b, args에 각각 어떤 값이 들어왔는지 확인한다. console.log(a, b, args); // 먼저 a와 b를 더해서 합계를 시작한다. let sum = a + b; // args 배열에 들어 있는 나머지 숫자들을 하나씩 더한다. for (let i = 0; i < args.length; i++) { sum += args[i]; } // 최종 합계를 함수 밖으로 반환한다. return sum; } let sum = add(1, 2, 3, 4, 5); console.log(sum);
add()함수는 매개변수를 세 종류로 받는다.
첫 번째 매개변수a는 첫 번째 인자를 받는다.
두 번째 매개변수b는 두 번째 인자를 받는다.
세 번째...args는 그 뒤에 남은 인자들을 배열로 모아 받는다.
add(1, 2, 3, 4, 5)를 호출하면 값은 아래처럼 들어간다.
a에는1이 들어간다.b에는2가 들어간다.args에는[3, 4, 5]가 들어간다.따라서
console.log(a, b, args)는1,2,[3, 4, 5]를 출력한다.
그다음let sum = a + b에서1 + 2를 먼저 계산한다.
그래서sum은3으로 시작한다.
이후for반복문이args배열을 돌면서3,4,5를 차례대로 더한다.
최종 합계는1 + 2 + 3 + 4 + 5이므로15가 된다.
출력 결과는 아래처럼 볼 수 있다.// 출력결과 // 1 2 [3, 4, 5] // 15브라우저 콘솔에서 배열 표시 모양은 실행 환경에 따라 조금 다르게 보일 수 있다.
하지만 핵심은args에[3, 4, 5]가 들어간다는 점이다.
function add(a, b, ...args)에서...args는 앞의 매개변수들이 받은 값을 제외한 나머지 인자를 배열로 모은다.
add1 함수는 모든 값을 args 배열로 받는다
// exam6.html // 전달된 모든 값을 args 배열 하나로 받는다. function add1(...args) { // args에 어떤 값들이 배열로 들어왔는지 확인한다. console.log(args); // 합계를 0부터 시작한다. let sum = 0; // args 배열에 들어 있는 모든 숫자를 하나씩 더한다. for (let i = 0; i < args.length; i++) { sum += args[i]; } // 최종 합계를 함수 밖으로 반환한다. return sum; } sum = add1(1, 2, 3, 4, 5); console.log(sum);
add1()함수는 매개변수가...args하나뿐이다.
따라서 전달된 모든 인자가args배열에 들어간다.
add1(1, 2, 3, 4, 5)를 호출하면args에는[1, 2, 3, 4, 5]가 들어간다.
앞에서 본add()와 다른 점은a,b처럼 따로 받는 값이 없다는 것이다.
그래서sum은0부터 시작한다.
그다음for반복문이args배열의 모든 값을 하나씩 더한다.
결국1 + 2 + 3 + 4 + 5가 되어 최종 결과는15가 된다.
출력 결과는 아래처럼 볼 수 있다.// 출력결과 // [1, 2, 3, 4, 5] // 15
add()와add1()은 둘 다 최종 합계가15이다.
하지만 값이 매개변수에 들어가는 방식은 다르다.
정리하면 아래처럼 구분할 수 있다.
add(a, b, ...args)는 앞의 두 값은a,b로 따로 받고 나머지를args로 받는다.add1(...args)는 모든 값을args하나로 받는다.
...args가 매개변수 목록의 어디에 있느냐에 따라 배열로 모이는 값의 범위가 달라진다.
Rest 매개변수는 마지막에 와야 한다
Rest매개변수는 남은 값을 모으는 역할을 한다.
그래서 매개변수 목록의 마지막에 작성해야 한다.
예제의add(a, b, ...args)를 보면...args가 가장 마지막에 있다.
이 구조가 자연스럽다.
왜냐하면a와b가 먼저 값을 받고, 그 뒤에 남은 값들을args가 모아야 하기 때문이다.
반대로 아래처럼 쓰면 안 된다.// exam6.html // 비교용 예시이다. function wrongAdd(...args, a, b) { // Rest 매개변수 뒤에 다른 매개변수를 둘 수 없다. return args; }이 코드는 문법 오류가 난다.
...args가 이미 남은 값을 전부 가져가려고 하는데, 그 뒤에a,b가 또 있으면 어떤 값을 어디에 넣어야 할지 정할 수 없기 때문이다.
Rest매개변수는 남은 인자를 모으는 역할이므로 항상 매개변수 목록의 마지막에 와야 한다.
add2 함수는 기본값 매개변수를 사용한다
// exam6.html // b에 기본값 10을 지정한다. function add2(a, b = 10) { // a에 들어온 값을 출력한다. console.log("a = " + a); // b에 들어온 값 또는 기본값을 출력한다. console.log("b = " + b); // a와 b를 더한 결과를 반환한다. return a + b; }
add2()함수는Rest매개변수를 사용하지 않는다.
대신b = 10처럼 두 번째 매개변수b에 기본값을 지정한다.
기본값 매개변수는 인자가 전달되지 않았을 때만 사용된다.
즉,b자리에 값이 없으면10이 들어간다.
하지만b자리에 값을 직접 전달하면 그 값이 사용된다.
기본값은 항상 사용되는 값이 아니라, 값이 없을 때만 대신 들어가는 값이다.
두 번째 인자를 생략하면 기본값이 사용된다
// exam6.html // 두 번째 인자를 생략했으므로 b에는 기본값 10이 들어간다. sum = add2(1); console.log(sum); // add2 함수가 반환한 합계를 출력한다.
add2(1)은 인자를 하나만 전달한다.
그래서a에는1이 들어간다.
하지만b에 전달된 값은 없다.
이때b = 10으로 기본값이 지정되어 있으므로b에는10이 들어간다.
따라서return a + b는1 + 10이 되고, 결과는11이다.
출력 결과는 아래처럼 볼 수 있다.// 출력결과 // a = 1 // b = 10 // 11기본값 매개변수는 해당 인자가 전달되지 않았을 때만 기본값을 사용한다.
두 번째 인자를 직접 전달하면 기본값은 사용되지 않는다
// exam6.html // 두 번째 인자를 직접 전달했으므로 기본값 10은 사용되지 않는다. console.log(add2(100, 200));이번에는
add2(100, 200)을 호출한다.
이 경우a에는100이 들어간다.
b에는 직접 전달한200이 들어간다.
이미b자리에 값이 전달되었기 때문에 기본값10은 사용되지 않는다.
그래서return a + b는100 + 200이 되고, 결과는300이다.
출력 결과는 아래처럼 볼 수 있다.// 출력결과 // a = 100 // b = 200 // 300이 결과를 보면 기본값이 언제 사용되는지 분명해진다.
값이 없을 때는 기본값이 사용된다.
값이 있으면 직접 전달한 값이 사용된다.
기본값은 전달값보다 우선하지 않는다.
전달값이 있으면 전달값이 사용된다.
Rest 매개변수와 기본값 매개변수 비교하기
Rest매개변수와 기본값 매개변수는 모두 함수 매개변수와 관련된 문법이다.
하지만 해결하려는 문제가 다르다.
Rest매개변수는 값의 개수가 많거나 정해져 있지 않을 때 사용한다.
전달된 여러 값을 배열로 모아 처리할 수 있다.
기본값 매개변수는 값이 생략될 수 있을 때 사용한다.
값이 전달되지 않아도 기본값으로 계산을 이어갈 수 있다.
정리하면 아래처럼 볼 수 있다.
Rest매개변수는 여러 값을 배열로 모은다.- 기본값 매개변수는 값이 없을 때 사용할 값을 정한다.
...args는 전달된 여러 인자를 배열로 받는다.b = 10은b가 없을 때10을 대신 넣는다.Rest매개변수는 매개변수 목록의 마지막에 와야 한다.- 기본값 매개변수는 전달값이 있으면 기본값을 사용하지 않는다.
Rest매개변수는 인자가 많을 때 유용하고, 기본값 매개변수는 인자가 생략될 수 있을 때 유용하다.
핵심 정리
이 예제의 핵심은 함수가 받는 인자를 더 유연하게 처리하는 방법이다.
...args는 여러 인자를 배열로 모으고,b = 10은 값이 없을 때 기본값을 사용한다.
이 흐름은 아래처럼 정리할 수 있다.
function add(a, b, ...args)는 앞의 두 값은a,b로 받고 나머지는args배열로 받는다.add(1, 2, 3, 4, 5)를 호출하면a는1,b는2,args는[3, 4, 5]가 된다.function add1(...args)는 전달된 모든 인자를args배열로 받는다.add1(1, 2, 3, 4, 5)를 호출하면args는[1, 2, 3, 4, 5]가 된다.Rest매개변수는 항상 매개변수 목록의 마지막에 와야 한다.function add2(a, b = 10)은b에 기본값10을 지정한다.add2(1)처럼 두 번째 인자를 생략하면b는10이 된다.add2(100, 200)처럼 두 번째 인자를 전달하면b는200이 된다.- 기본값은 인자가 없을 때만 사용된다.
따라서 이 예제가 알려 주는 핵심은 이것이다.
Rest매개변수는 여러 인자를 배열로 모아 처리하고, 기본값 매개변수는 인자가 생략되었을 때 사용할 값을 미리 정해 함수 호출을 더 유연하게 만들어 준다.
이 예제는
template literal을 사용해서 문자열을 작성하는 방법을 보여 준다.
template literal은 문자열을 만들 때 작은따옴표나 큰따옴표 대신 백틱을 사용하는 문법이다.
백틱은 키보드에서 보통 숫자1왼쪽에 있는 기호이다.
기존 문자열은 줄바꿈을 그대로 넣거나, 문자열 안에 변수와 계산식을 섞어 쓰기가 불편했다.
하지만template literal을 사용하면 줄바꿈을 문자열 안에 그대로 담을 수 있고,${}안에 변수나 계산식을 넣을 수 있다.
template literal의 핵심은 문자열 안에 변수, 계산식, 줄바꿈을 자연스럽게 넣을 수 있다는 점이다.
템플릿 문자열이란 무엇인가
template literal은 백틱으로 감싸서 만드는 문자열이다.
일반 문자열은 보통"문자열"또는'문자열'처럼 큰따옴표나 작은따옴표를 사용한다.
하지만template literal은 백틱을 사용한다.
예를 들면 아래와 같은 형태이다.// exam7.html // 비교용 예시이다. let str = `안녕하세요`;이렇게 작성하면
str변수에"안녕하세요"라는 문자열이 저장된다.
여기까지만 보면 일반 문자열과 큰 차이가 없어 보일 수 있다.
하지만template literal은 일반 문자열보다 더 편한 점이 있다.
- 줄바꿈을 문자열 안에 그대로 넣을 수 있다.
${}안에 변수를 넣을 수 있다.${}안에 계산식을 넣을 수 있다.- 문자열 연결 연산자
+를 많이 쓰지 않아도 된다.
template literal은 문자열을 더 읽기 쉽고 관리하기 쉽게 작성하기 위한 문법이다.
템플릿 문자열이 필요한 이유
문자열 안에 변수 값을 넣어야 하는 경우는 자주 생긴다.
예를 들어 숫자 두 개를 곱한 결과를"3 * 10 = 30"처럼 출력하고 싶다고 해 보자.
기존 방식에서는 문자열과 변수를+로 계속 연결해야 한다.
값이 많아질수록 코드가 길어지고, 어디가 문자열이고 어디가 변수인지 한눈에 보기 어려워진다.
반면template literal은 문자열 안에${firstNumber}처럼 변수를 바로 넣을 수 있다.
그래서 출력하려는 문장 모양과 코드 모양이 비슷해진다.
문자열 결과의 모양을 코드에서 그대로 볼 수 있기 때문에 초보자도 읽기 쉽다.
예제 전체 코드
// exam7.html <!doctype html> <html> <head> <meta charset="UTF-8" /> <title>ECMAScript6</title> </head> <body> <h1>템플릿 문자열</h1> <hr /> <script> // 1. 백틱으로 한 줄 문자열을 만든다. let str = `3 * 1 = 3`; // str에 저장된 문자열을 출력한다. console.log(str); // 2. 백틱 문자열 안에 줄바꿈을 그대로 넣는다. str = `3 * 1 = 3 3 * 2 = 6 3 * 3 = 9`; // 줄바꿈이 포함된 문자열을 출력한다. console.log(str); // 3. 문자열 안에 넣을 숫자 값을 변수에 저장한다. let firstNumber = 3; let secondNumber = 10; // 4. ${} 안에 변수를 넣어 문자열을 만든다. // 결과 값 30은 직접 작성했다. str = `${firstNumber} * ${secondNumber} = 30`; // 변수 값이 들어간 문자열을 출력한다. console.log(str); // 5. ${} 안에 계산식을 넣어 문자열을 만든다. str = `${firstNumber} * ${secondNumber} = ${firstNumber * secondNumber}`; // 변수와 계산식이 함께 들어간 문자열을 출력한다. console.log(str); </script> </body> </html>이 코드는 크게 네 부분으로 나누어 볼 수 있다.
첫 번째는 백틱으로 한 줄 문자열을 만드는 부분이다.
두 번째는 백틱 문자열 안에 줄바꿈을 그대로 넣는 부분이다.
세 번째는${}안에 변수를 넣는 부분이다.
네 번째는${}안에 계산식을 넣는 부분이다.
이 예제에서 중요한 부분은 문자열을 만드는 방식이다.
작은따옴표나 큰따옴표를 쓰지 않고 백틱을 사용한다.
그리고 문자열 안에 값을 넣고 싶은 위치에는${}를 사용한다.
template literal은 문자열 안에 값을 끼워 넣는 위치를${}로 표시한다.
이 예제에서 확인할 핵심
template literal은 백틱으로 만드는 문자열이다.- 백틱 문자열은 줄바꿈을 그대로 포함할 수 있다.
${변수}를 사용하면 문자열 안에 변수 값을 넣을 수 있다.${계산식}을 사용하면 문자열 안에 계산 결과를 넣을 수 있다.${firstNumber}는 변수firstNumber의 값을 문자열 안에 넣는다.${firstNumber * secondNumber}는 계산 결과를 문자열 안에 넣는다.- 문자열 안에 값을 넣는 방식을 문자열 보간이라고 부를 수 있다.
이 예제는 문자열을 단순히 적는 것에서 끝나지 않고, 변수와 계산식을 문자열 안에 자연스럽게 섞는 방법을 보여 준다.
백틱으로 한 줄 문자열 만들기
// exam7.html // 백틱으로 한 줄 문자열을 만든다. let str = `3 * 1 = 3`; console.log(str); // str에 저장된 문자열을 출력한다.이 코드는
str변수에3 * 1 = 3이라는 문자열을 저장한다.
여기서 문자열을 감싼 기호는 큰따옴표가 아니라 백틱이다.
백틱으로 감싼 문자열도 문자열이다.
그래서console.log(str)를 실행하면str안에 저장된 문자열이 그대로 출력된다.
출력 결과는 아래와 같다.// 출력결과 // 3 * 1 = 3이 부분에서는 아직 변수나 계산식이 들어가지 않았다.
백틱으로 문자열을 만들 수 있다는 기본 형태를 먼저 보여 주는 코드이다.
백틱으로 감싼 값도 문자열로 처리된다.
백틱 문자열 안에 줄바꿈 넣기
// exam7.html // 백틱 문자열 안에 줄바꿈을 그대로 넣는다. str = `3 * 1 = 3 3 * 2 = 6 3 * 3 = 9`; console.log(str); // 줄바꿈이 포함된 문자열을 출력한다.이 코드는 문자열 안에 줄바꿈을 그대로 넣었다.
3 * 1 = 3,3 * 2 = 6,3 * 3 = 9가 각각 다른 줄에 작성되어 있다.
template literal은 백틱 안에서 줄바꿈을 그대로 유지한다.
그래서 출력할 때도 여러 줄로 출력된다.
출력 결과는 아래와 같다.// 출력결과 // 3 * 1 = 3 // 3 * 2 = 6 // 3 * 3 = 9기존 문자열에서는 줄바꿈을 표현하려면
\n같은 특수 문자를 사용해야 하는 경우가 많았다.
하지만template literal에서는 실제 줄바꿈을 코드에 그대로 작성할 수 있다.
백틱 문자열은 줄바꿈을 문자열 안에 그대로 담을 수 있다.
이 점 때문에 여러 줄 문자열을 만들 때 편리하다.
문자열 안에 변수 넣기
// exam7.html // 문자열 안에 넣을 숫자 값을 변수에 저장한다. let firstNumber = 3; let secondNumber = 10; // ${} 안에 변수를 넣어 문자열을 만든다. // 결과 값 30은 직접 작성했다. str = `${firstNumber} * ${secondNumber} = 30`; console.log(str); // 변수 값이 들어간 문자열을 출력한다.이 코드는
firstNumber와secondNumber값을 문자열 안에 넣는다.
firstNumber에는3이 저장되어 있고,secondNumber에는10이 저장되어 있다.
${firstNumber}는firstNumber변수의 값을 문자열 안에 넣으라는 뜻이다.
그래서${firstNumber}부분은3으로 바뀐다.
${secondNumber}는secondNumber변수의 값을 문자열 안에 넣으라는 뜻이다.
그래서${secondNumber}부분은10으로 바뀐다.
결과적으로 만들어지는 문자열은3 * 10 = 30이다.// 출력결과 // 3 * 10 = 30여기서 주의할 점이 있다.
마지막의30은 계산된 값이 아니다.
코드에 직접 적어 둔 문자열이다.
즉, 이 코드는 변수 값은 문자열 안에 넣지만, 결과30은 직접 작성한 값이다.
그래서firstNumber나secondNumber가 바뀌면 마지막30도 직접 고쳐야 한다.
${}안에 넣은 부분만 실제 변수 값으로 바뀐다.
그 밖에 그냥 적은 글자는 그대로 문자열이 된다.
문자열 안에 계산식 넣기
// exam7.html // ${} 안에 계산식을 넣어 문자열을 만든다. str = `${firstNumber} * ${secondNumber} = ${firstNumber * secondNumber}`; console.log(str); // 변수와 계산식이 함께 들어간 문자열을 출력한다.이번에는 마지막 결과도 직접 쓰지 않고 계산식으로 넣었다.
${firstNumber * secondNumber}는firstNumber와secondNumber를 곱한 결과를 문자열 안에 넣으라는 뜻이다.
현재firstNumber는3이고secondNumber는10이다.
따라서firstNumber * secondNumber는30이다.
그래서 최종 문자열은3 * 10 = 30이 된다.// 출력결과 // 3 * 10 = 30앞의 예제와 출력 결과는 같다.
하지만 만드는 방식은 다르다.
앞의 예제는 마지막30을 직접 적었다.
이번 예제는${firstNumber * secondNumber}로 계산한 결과를 넣었다.
${}안에는 변수뿐 아니라 계산식도 넣을 수 있다.
이렇게 작성하면 변수 값이 바뀌어도 계산 결과가 자동으로 반영된다.
변수 직접 작성과 계산식 작성 비교하기
두 코드는 출력 결과가 같다.
하지만 코드의 의미는 다르다.// exam7.html // 결과 값 30을 문자열로 직접 작성한 코드이다. str = `${firstNumber} * ${secondNumber} = 30`; // 계산 결과를 ${} 안에서 직접 구하는 코드이다. str = `${firstNumber} * ${secondNumber} = ${firstNumber * secondNumber}`;첫 번째 코드는 마지막
30을 직접 문자열로 작성했다.
그래서 실제 계산을 하지 않는다.
그냥30이라는 글자를 문자열 안에 넣은 것이다.
두 번째 코드는${firstNumber * secondNumber}를 사용했다.
그래서 실제로3 * 10을 계산한 뒤 그 결과를 문자열 안에 넣는다.
초보자 입장에서는 두 코드가 모두3 * 10 = 30을 출력하므로 같아 보일 수 있다.
하지만 유지보수 관점에서는 두 번째 방식이 더 안전하다.
변수 값이 바뀌어도 계산 결과가 함께 바뀌기 때문이다.
문자열 안의 결과가 변수와 연결되어야 한다면, 직접 적는 것보다${}안에 계산식을 넣는 것이 좋다.
전체 출력 결과
이 예제의 전체 출력 결과는 아래처럼 볼 수 있다.
// 출력결과 // 3 * 1 = 3 // 3 * 1 = 3 // 3 * 2 = 6 // 3 * 3 = 9 // 3 * 10 = 30 // 3 * 10 = 30첫 번째 줄은 한 줄 문자열을 출력한 결과이다.
그다음 세 줄은 줄바꿈이 포함된 문자열을 출력한 결과이다.
마지막 두 줄은 변수와 계산식을 사용해 만든 문자열의 결과이다.
마지막 두 출력 결과는 둘 다3 * 10 = 30이다.
하지만 하나는 마지막30을 직접 작성한 문자열이고, 다른 하나는 계산식 결과를 넣은 문자열이다.
출력 결과가 같아도 문자열을 만든 방식이 다를 수 있다.
이 차이를 이해해야template literal을 제대로 사용할 수 있다.
템플릿 문자열과 기존 문자열 연결 비교하기
기존 방식에서는 문자열과 변수를
+로 연결하는 경우가 많았다.
아래 코드는 비교용 예시이다.// exam7.html // 비교용 예시이다. let firstNumber = 3; let secondNumber = 10; let str = firstNumber + " * " + secondNumber + " = " + firstNumber * secondNumber; console.log(str);// 출력결과 // 3 * 10 = 30이 코드도 결과는 같다.
하지만+가 많아서 읽기 어렵다.
어디까지가 문자열이고 어디부터가 변수인지 한눈에 보기 어렵다.
같은 내용을template literal로 쓰면 아래처럼 더 자연스럽다.// exam7.html // 비교용 예시이다. let str = `${firstNumber} * ${secondNumber} = ${firstNumber * secondNumber}`; console.log(str);// 출력결과 // 3 * 10 = 30
template literal을 사용하면 실제 출력될 문장 모양과 코드 모양이 거의 비슷해진다.
그래서 문자열 안에 변수가 많아질수록 코드가 더 읽기 쉬워진다.
변수나 계산식이 섞인 문자열은+로 이어 붙이는 방식보다template literal로 작성하는 것이 더 읽기 쉽다.
핵심 정리
이 예제의 핵심은
template literal을 사용해 문자열을 더 편하게 만드는 방법이다.
백틱으로 문자열을 감싸면 줄바꿈을 그대로 넣을 수 있고,${}안에 변수나 계산식을 넣을 수 있다.
이 흐름은 아래처럼 정리할 수 있다.
template literal은 백틱으로 만드는 문자열이다.- 백틱 문자열은 줄바꿈을 그대로 포함할 수 있다.
${firstNumber}는 변수firstNumber의 값을 문자열 안에 넣는다.${secondNumber}는 변수secondNumber의 값을 문자열 안에 넣는다.${firstNumber * secondNumber}는 계산 결과를 문자열 안에 넣는다.- 결과
30을 직접 적으면 계산이 아니라 문자열로 적은 값이 그대로 들어간다.${firstNumber * secondNumber}처럼 작성하면 실제 계산 결과가 문자열 안에 들어간다.- 출력 결과가 같아도 문자열을 만든 방식은 다를 수 있다.
- 변수나 계산식이 들어가는 문자열은
template literal로 쓰면 더 읽기 쉽다.따라서 이 예제가 알려 주는 핵심은 이것이다.
template literal은 문자열 안에 변수, 계산식, 줄바꿈을 자연스럽게 넣어 문자열 작성 코드를 더 읽기 쉽게 만들어 주는 문법이다.
이 예제는
Spread연산자를 사용해서 배열이나 문자열처럼 여러 값을 가진 대상을 펼쳐 사용하는 흐름을 보여 준다.
Spread는 “펼치다”라는 뜻이다.
즉, 배열 안에 묶여 있는 값들을 하나씩 풀어서 사용하는 문법이다.
앞에서 본Rest매개변수도...기호를 사용했다.
하지만Rest는 여러 값을 배열로 모으는 역할이고,Spread는 배열이나 문자열처럼 묶여 있는 값을 펼치는 역할이다.
같은...기호를 사용해도, 값을 모으면Rest이고 값을 펼치면Spread이다.
exam8.html에서는Spread연산자로 배열을 복사하거나, 새 배열 안에 기존 배열 값을 펼쳐 넣거나, 함수 인자로 배열 값을 하나씩 전달하는 흐름을 확인한다.
exam9.html에서는push()와Math.max()에서Spread연산자를 사용했을 때와 사용하지 않았을 때의 차이를 확인한다.
Spread 연산자란 무엇인가
Spread연산자는 배열이나 문자열처럼 여러 값을 담고 있는 대상을 펼쳐서 개별 값처럼 사용하는 문법이다.
문법은...대상형태로 작성한다.
예를 들어 배열[1, 2, 3]이 있다고 해 보자.
이 배열은 값 세 개가 하나의 배열 안에 묶여 있는 상태이다.
여기에...를 붙여...array처럼 사용하면 배열 안의 값이1,2,3처럼 하나씩 펼쳐진다.
아래처럼 이해하면 된다.// exam8.html // 비교용 예시이다. const array = [1, 2, 3]; // ...array는 1, 2, 3처럼 배열 안의 값을 펼치는 의미이다. console.log(...array);// 출력결과 // 1 2 3여기서 중요한 점은
array와...array가 다르다는 것이다.
array는 배열 하나이다.
...array는 배열 안의 값을 하나씩 펼친 것이다.
Spread연산자는 묶여 있는 값을 풀어서 개별 값처럼 사용할 때 쓰는 문법이다.
Spread와 Rest는 같은 기호지만 역할이 다르다
Spread와Rest는 모두...기호를 사용한다.
그래서 처음 보면 같은 문법처럼 보일 수 있다.
하지만 역할은 반대이다.
Rest는 여러 값을 하나의 배열로 모은다.
함수 매개변수 자리에서...args처럼 사용하면, 남은 인자들이args배열로 모인다.
반대로Spread는 배열이나 문자열처럼 이미 묶여 있는 값을 펼친다.
배열 앞에...를 붙이면 배열 안의 값들이 밖으로 하나씩 나온다.
정리하면 아래처럼 구분할 수 있다.
Rest는 여러 값을 배열로 모은다.Spread는 배열이나 문자열 안의 값을 펼친다.- 함수 매개변수 자리의
...args는 주로Rest이다.- 배열 리터럴이나 함수 호출 인자 자리의
...array는 주로Spread이다.
...기호를 볼 때는 위치를 먼저 봐야 한다. 값을 받는 자리에서 모으면Rest, 값을 사용하는 자리에서 펼치면Spread이다.
예제 전체 코드 1
// exam8.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>ECMAScript6</title> </head> <body> <h1>Spread 연산자(1)</h1> <hr /> <script> // 색상 배열을 만든다. const colorArray = ["red", "green", "blue"]; // 기존 배열 값을 펼친 뒤 orange를 뒤에 추가해서 새 배열을 만든다. const newColorArray = [...colorArray, "orange"]; // 원래 배열을 출력한다. console.log(colorArray); // 새로 만든 배열을 출력한다. console.log(newColorArray); // 숫자 배열을 만든다. const numArray = [2, 3, 4]; // 1을 앞에 넣고, numArray 값을 펼친 뒤, 5를 뒤에 넣어 새 배열을 만든다. const newNumArray = [1, ...numArray, 5]; // 원래 숫자 배열을 출력한다. console.log(numArray); // 새로 만든 숫자 배열을 출력한다. console.log(newNumArray); // 값 세 개를 받아 각각 출력하는 함수이다. function myPrint(a, b, c) { console.log(a); console.log(b); console.log(c); } // 숫자 세 개가 들어 있는 배열이다. const array = [1, 2, 3]; // 배열 자체를 출력한다. console.log(array); // 배열 안의 값을 펼쳐서 함수 인자로 전달한다. myPrint(...array); // 문자열 xyz를 문자 단위로 펼쳐 배열로 만든다. const stringArray = [..."xyz"]; // 문자열에서 만들어진 배열을 출력한다. console.log(stringArray); // 문자열 배열의 값을 펼쳐서 함수 인자로 전달한다. myPrint(...stringArray); </script> </body> </html>이 코드는 크게 네 부분으로 나누어 볼 수 있다.
첫 번째는 기존 배열 값을 펼쳐 새 배열을 만드는 부분이다.
두 번째는 배열 중간에 기존 배열 값을 펼쳐 넣는 부분이다.
세 번째는 배열 값을 함수 인자로 펼쳐 전달하는 부분이다.
네 번째는 문자열을 문자 단위로 펼쳐 배열로 만드는 부분이다.
이 예제에서 중요한 점은 기존 배열 자체가 바뀌는 것이 아니라는 점이다.
newColorArray와newNumArray는 기존 배열 값을 펼쳐서 만든 새 배열이다.
원래 배열인colorArray,numArray는 그대로 유지된다.
Spread연산자는 기존 값을 펼쳐 새 배열을 만들거나 함수 인자처럼 개별 값이 필요한 자리에 전달할 때 사용한다.
예제 전체 코드 2
// exam9.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>ECMAScript6</title> </head> <body> <h1>Spread 연산자(2)</h1> <hr /> <script> // 문자열 값들이 들어 있는 배열을 만든다. const array1 = ["a", "b", "c"]; // push에 값을 여러 개 직접 전달한다. array1.push("x", "y", "z"); // 배열 안의 값을 펼쳐서 push에 전달한다. array1.push(...["d", "e"]); // 배열 자체를 하나의 값으로 push에 전달한다. array1.push(["w", "v"]); // 최종 배열을 출력한다. console.log(array1); // 숫자 배열을 만든다. const array2 = [10, 20, 60, 50, 30]; // 배열 안의 값을 펼쳐 Math.max에 전달한다. console.log(Math.max(...array2)); // 배열 자체를 Math.max에 전달한다. console.log(Math.max(array2)); </script> </body> </html>이 코드는
Spread연산자를 사용했을 때와 사용하지 않았을 때의 차이를 보여 준다.
array1.push(...["d", "e"])는 배열 안의"d","e"를 펼쳐서 각각 추가한다.
반면array1.push(["w", "v"])는 배열 자체를 하나의 값으로 추가한다.
또Math.max(...array2)는 배열 안의 숫자들을 펼쳐서 전달하므로 가장 큰 값인60을 찾을 수 있다.
하지만Math.max(array2)는 배열 자체를 하나의 인자로 전달하므로 원하는 숫자 비교가 되지 않는다.
함수가 개별 값을 필요로 할 때 배열을 그대로 전달하면 안 되고,Spread연산자로 값을 펼쳐 전달해야 한다.
이 예제에서 확인할 핵심
Spread연산자는...기호를 사용한다.- 배열 앞에
...를 붙이면 배열 안의 값이 펼쳐진다.- 문자열 앞에
...를 붙이면 문자열이 문자 단위로 펼쳐진다.[...colorArray, "orange"]는 기존 배열 값을 펼쳐 새 배열을 만든다.[1, ...numArray, 5]는 배열 중간에 기존 배열 값을 펼쳐 넣는다.myPrint(...array)는 배열 안의 값을 함수 인자로 하나씩 전달한다.push(...배열)은 배열 안의 값을 각각 추가한다.push(배열)은 배열 자체를 하나의 값으로 추가한다.Math.max(...array2)는 배열 안의 숫자들을 펼쳐 가장 큰 값을 구한다.Math.max(array2)는 배열 자체를 전달하므로 원하는 결과가 나오지 않는다.이 예제는
Spread연산자가 배열이나 문자열을 복사, 추가, 함수 호출에 어떻게 활용되는지 보여 준다.
기존 배열을 펼쳐 새 배열 만들기
// exam8.html // 색상 배열을 만든다. const colorArray = ["red", "green", "blue"]; // 기존 배열 값을 펼친 뒤 orange를 뒤에 추가해서 새 배열을 만든다. const newColorArray = [...colorArray, "orange"]; console.log(colorArray); // 원래 배열을 출력한다. console.log(newColorArray); // 새로 만든 배열을 출력한다.
colorArray에는"red","green","blue"가 들어 있다.
newColorArray는 이 기존 배열 값을 펼친 뒤"orange"를 추가해서 만든 새 배열이다.
[...colorArray, "orange"]에서...colorArray는colorArray안의 값을 하나씩 펼친다.
그래서 새 배열은"red","green","blue","orange"순서로 만들어진다.
출력 결과는 아래처럼 볼 수 있다.// 출력결과 // ['red', 'green', 'blue'] // ['red', 'green', 'blue', 'orange']여기서 중요한 점은 원래 배열이 바뀐 것이 아니라는 점이다.
colorArray는 그대로"red","green","blue"만 가진다.
newColorArray만"orange"가 추가된 새 배열이다.
Spread연산자를 사용하면 기존 배열을 직접 바꾸지 않고, 기존 값을 바탕으로 새 배열을 만들 수 있다.
배열 중간에 기존 배열 값 펼쳐 넣기
// exam8.html // 숫자 배열을 만든다. const numArray = [2, 3, 4]; // 1을 앞에 넣고, numArray 값을 펼친 뒤, 5를 뒤에 넣어 새 배열을 만든다. const newNumArray = [1, ...numArray, 5]; console.log(numArray); // 원래 숫자 배열을 출력한다. console.log(newNumArray); // 새로 만든 숫자 배열을 출력한다.
numArray에는2,3,4가 들어 있다.
newNumArray는 앞에1을 넣고, 그다음numArray의 값을 펼치고, 마지막에5를 넣어 만든 배열이다.
[1, ...numArray, 5]는 아래처럼 해석할 수 있다.
- 먼저
1을 넣는다.numArray안의2,3,4를 펼쳐 넣는다.- 마지막에
5를 넣는다.그래서 새 배열은
[1, 2, 3, 4, 5]가 된다.
출력 결과는 아래와 같다.// 출력결과 // [2, 3, 4] // [1, 2, 3, 4, 5]이 코드도 원래 배열
numArray를 바꾸지 않는다.
기존 배열 값을 사용해서 새 배열만 만든다.
Spread연산자는 배열 앞, 중간, 뒤 어디에든 사용할 수 있고, 그 위치에 배열 값들이 펼쳐져 들어간다.
배열 값을 함수 인자로 펼쳐 전달하기
// exam8.html // 값 세 개를 받아 각각 출력하는 함수이다. function myPrint(a, b, c) { console.log(a); console.log(b); console.log(c); } // 숫자 세 개가 들어 있는 배열이다. const array = [1, 2, 3]; console.log(array); // 배열 자체를 출력한다. // 배열 안의 값을 펼쳐서 함수 인자로 전달한다. myPrint(...array);
myPrint()함수는a,b,c세 개의 값을 따로 받는다.
즉,myPrint(1, 2, 3)처럼 값을 개별 인자로 전달해야 한다.
그런데 현재 값은[1, 2, 3]이라는 배열 안에 묶여 있다.
이때myPrint(...array)처럼 작성하면 배열 안의 값이 펼쳐져서 함수에 전달된다.
즉, 아래 두 코드는 같은 의미로 볼 수 있다.// exam8.html // 비교용 예시이다. myPrint(...array); myPrint(1, 2, 3);출력 결과는 아래와 같다.
// 출력결과 // [1, 2, 3] // 1 // 2 // 3첫 번째 출력은
console.log(array)의 결과이다.
그다음1,2,3은myPrint(...array)가 배열 안의 값을 펼쳐 전달했기 때문에 각각 출력된 결과이다.
함수가 여러 인자를 따로 받을 때 배열 값을 전달하려면...array처럼 펼쳐서 전달할 수 있다.
문자열을 펼쳐 배열로 만들기
// exam8.html // 문자열 xyz를 문자 단위로 펼쳐 배열로 만든다. const stringArray = [..."xyz"]; console.log(stringArray); // 문자열에서 만들어진 배열을 출력한다. // 문자열 배열의 값을 펼쳐서 함수 인자로 전달한다. myPrint(...stringArray);
Spread연산자는 배열뿐만 아니라 문자열에도 사용할 수 있다.
문자열"xyz"는 문자"x","y","z"가 순서대로 모여 있는 값이다.
[..."xyz"]는 문자열을 문자 단위로 펼쳐서 배열로 만든다.
그래서stringArray는["x", "y", "z"]가 된다.
그다음myPrint(...stringArray)는"x","y","z"를 함수 인자로 하나씩 전달한다.
출력 결과는 아래와 같다.// 출력결과 // ['x', 'y', 'z'] // x // y // z문자열에
Spread연산자를 사용하면 문자열을 문자 단위로 펼칠 수 있다.
push에서 Spread를 사용한 경우와 사용하지 않은 경우
// exam9.html // 문자열 값들이 들어 있는 배열을 만든다. const array1 = ["a", "b", "c"]; // push에 값을 여러 개 직접 전달한다. array1.push("x", "y", "z"); // 배열 안의 값을 펼쳐서 push에 전달한다. array1.push(...["d", "e"]); // 배열 자체를 하나의 값으로 push에 전달한다. array1.push(["w", "v"]); console.log(array1); // 최종 배열을 출력한다.
push()는 배열의 뒤에 값을 추가하는 메서드이다.
array1.push("x", "y", "z")는"x","y","z"를 각각 배열 뒤에 추가한다.
array1.push(...["d", "e"])는["d", "e"]배열 안의 값을 펼쳐서 추가한다.
그래서"d"와"e"가 각각 따로 들어간다.
하지만array1.push(["w", "v"])는Spread연산자를 사용하지 않았다.
그래서"w"와"v"가 따로 들어가는 것이 아니라,["w", "v"]배열 자체가 하나의 값으로 들어간다.
출력 결과는 아래처럼 볼 수 있다.// 출력결과 // ['a', 'b', 'c', 'x', 'y', 'z', 'd', 'e', ['w', 'v']]여기서 마지막 값은 문자열
"w","v"두 개가 따로 들어간 것이 아니다.
["w", "v"]라는 배열 하나가 마지막 요소로 들어간 것이다.
push(...배열)은 배열 안의 값을 각각 추가하고,push(배열)은 배열 자체를 하나의 값으로 추가한다.
이 차이를 반드시 구분해야 한다.
Math.max에서 Spread가 필요한 이유
// exam9.html // 숫자 배열을 만든다. const array2 = [10, 20, 60, 50, 30]; // 배열 안의 값을 펼쳐 Math.max에 전달한다. console.log(Math.max(...array2)); // 배열 자체를 Math.max에 전달한다. console.log(Math.max(array2));
Math.max()는 여러 숫자 중 가장 큰 값을 구하는 메서드이다.
중요한 점은Math.max()가 배열 하나를 받는 것이 아니라, 숫자 여러 개를 따로 받는다는 것이다.
즉,Math.max(10, 20, 60, 50, 30)처럼 숫자들이 개별 인자로 들어와야 한다.
현재 값은[10, 20, 60, 50, 30]이라는 배열에 묶여 있다.
그래서Math.max(...array2)처럼 배열을 펼쳐서 전달해야 한다.
Math.max(...array2)는 아래와 같은 의미로 볼 수 있다.// exam9.html // 비교용 예시이다. Math.max(10, 20, 60, 50, 30);그래서 가장 큰 값인
60이 출력된다.
반면Math.max(array2)는 배열 자체를 하나의 인자로 전달한다.
Math.max()는 배열을 숫자 여러 개로 자동으로 풀어서 비교하지 않는다.
그래서 숫자 비교가 제대로 되지 않아NaN이 나온다.
NaN은Not a Number의 줄임말이다.
숫자가 아니라서 정상적인 숫자 결과를 만들 수 없다는 뜻이다.
출력 결과는 아래와 같다.// 출력결과 // 60 // NaN여러 숫자를 따로 받아야 하는 함수에 배열 값을 전달할 때는
Spread연산자로 배열을 펼쳐야 한다.
Spread 연산자를 사용할 때 주의할 점
Spread연산자는 배열 안의 값을 펼쳐 주기 때문에 편리하다.
하지만 배열이 필요한 자리에 쓰는 것이 아니라, 개별 값들이 필요한 자리에 사용해야 한다.
예를 들어myPrint()는 값 세 개를 따로 받는다.
그래서myPrint(...array)처럼 배열을 펼쳐 전달하는 것이 자연스럽다.
Math.max()도 숫자 여러 개를 따로 받는다.
그래서Math.max(...array2)처럼 배열 안의 숫자들을 펼쳐 전달해야 한다.
반대로 배열 자체를 하나의 값으로 넣고 싶다면Spread를 사용하면 안 된다.
array1.push(["w", "v"])처럼 배열 자체를 추가하고 싶을 때는...를 붙이지 않는다.
정리하면 아래처럼 볼 수 있다.
- 개별 값들이 필요한 자리에는
Spread를 사용한다.- 배열 자체를 하나의 값으로 넣고 싶다면
Spread를 사용하지 않는다.myPrint(...array)는 배열 값을 함수 인자로 펼친다.Math.max(...array2)는 배열 값을 숫자 인자로 펼친다.array1.push(...["d", "e"])는"d","e"를 각각 추가한다.array1.push(["w", "v"])는 배열 자체를 하나의 값으로 추가한다.
Spread를 사용할지 말지는 “배열을 통째로 넣을지, 배열 안의 값을 펼쳐 넣을지”를 기준으로 판단하면 된다.
핵심 정리
이 예제의 핵심은
Spread연산자가 묶여 있는 값을 펼쳐서 사용하는 문법이라는 점이다.
배열이나 문자열 앞에...를 붙이면 안에 있는 값들이 개별 값처럼 펼쳐진다.
이 흐름은 아래처럼 정리할 수 있다.
Spread연산자는...기호를 사용한다.Rest는 여러 값을 배열로 모으고,Spread는 묶인 값을 펼친다.[...colorArray, "orange"]는 기존 배열 값을 펼쳐 새 배열을 만든다.[1, ...numArray, 5]는 배열 중간에 기존 배열 값을 펼쳐 넣는다.myPrint(...array)는[1, 2, 3]을1,2,3처럼 펼쳐 전달한다.[..."xyz"]는 문자열을 문자 단위로 펼쳐["x", "y", "z"]배열을 만든다.array1.push(...["d", "e"])는"d","e"를 각각 추가한다.array1.push(["w", "v"])는 배열 자체를 하나의 값으로 추가한다.Math.max(...array2)는 배열 안의 숫자들을 펼쳐 가장 큰 값60을 구한다.Math.max(array2)는 배열 자체를 전달하므로NaN이 나온다.따라서 이 예제가 알려 주는 핵심은 이것이다.
Spread연산자는 배열이나 문자열처럼 묶여 있는 값을 개별 값처럼 펼쳐서 새 배열을 만들거나 함수 인자로 전달할 때 사용하는 문법이다.
이 예제는
JavaScript에서 객체를 만들 때 사용하는 객체 리터럴 문법을 보여 준다.
객체는 여러 값을 하나로 묶어 표현할 때 사용하는 구조이다.
예를 들어 커피 하나를 표현하려면 이름과 가격이 함께 필요하다.
이때name과price를 따로따로 관리하는 것보다 하나의 객체 안에 묶어 두면 더 자연스럽다.
exam10.html에서는 객체의 프로퍼티 이름을 직접 작성하는 경우와 변수 값을 사용해서 프로퍼티 이름을 만드는 경우를 비교한다.
exam11.html에서는 변수 이름과 프로퍼티 이름이 같을 때 더 짧게 객체를 만드는 방법을 확인한다.
그리고Object.keys(),Object.values(),Object.entries()로 객체 안의 구조를 확인한다.
이 예제의 핵심은 객체를 만들 때 프로퍼티 이름과 값을 어떻게 연결하는지 이해하는 것이다.
객체 리터럴이란 무엇인가
객체 리터럴은 중괄호
{}를 사용해서 객체를 바로 만드는 문법이다.
객체는 여러 값을 하나로 묶어 표현할 때 사용한다.
예를 들어 커피 하나를 표현하려면 커피 이름과 가격이 필요하다.
이때 이름은"Americano"이고 가격은3000이라고 해 보자.
이 두 값을 하나의 객체로 묶으면 아래처럼 표현할 수 있다.// exam10.html // 비교용 예시이다. const coffee = { name: "Americano", price: 3000 };여기서
name과price는 프로퍼티 이름이다.
프로퍼티는 객체 안에 들어 있는 값의 이름과 실제 값을 묶은 항목이라고 이해하면 된다.
name: "Americano"에서name은 값을 꺼낼 때 사용하는 이름이고,"Americano"는 실제 값이다.
price: 3000에서price는 값을 꺼낼 때 사용하는 이름이고,3000은 실제 값이다.
객체 리터럴은 중괄호 안에 프로퍼티 이름과 값을 적어서 객체를 만드는 문법이다.
객체가 필요한 이유
객체가 필요한 이유는 서로 관련 있는 값을 하나로 묶기 위해서이다.
커피 이름과 가격은 서로 관련 있는 값이다.
이름은 이름대로, 가격은 가격대로 따로 두면 값이 많아질수록 관리하기 어렵다.
객체를 사용하면 커피 하나에 대한 정보를 한 덩어리로 묶을 수 있다.
그래서coffee.name처럼 커피 객체에서 이름을 꺼내고,coffee.price처럼 커피 객체에서 가격을 꺼낼 수 있다.
정리하면 아래처럼 볼 수 있다.
- 객체는 관련 있는 값을 하나로 묶는다.
- 객체 안의 값은 프로퍼티로 저장된다.
- 프로퍼티는 이름과 실제 값으로 구성된다.
coffee.name은coffee객체의name값을 꺼낸다.coffee.price는coffee객체의price값을 꺼낸다.객체는 여러 값을 하나의 의미 있는 데이터로 묶어 관리하기 위해 사용한다.
예제 전체 코드 1
// exam10.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>ECMAScript6</title> </head> <body> <h1>객체리터럴에서 추가된 구문</h1> <hr /> <script> // 프로퍼티 이름을 따옴표 없이 작성한 객체이다. const coffee1 = { name: "Americano1", price: 3000 }; // 객체 전체와 각 프로퍼티 값을 출력한다. console.log(coffee1); console.log(coffee1.name); console.log(coffee1.price); // 프로퍼티 이름을 큰따옴표로 감싼 객체이다. const coffee2 = { "name": "Americano2", "price": 3000 }; // 객체 전체와 각 프로퍼티 값을 출력한다. console.log(coffee2); console.log(coffee2.name); console.log(coffee2.price); // 프로퍼티 이름을 작은따옴표로 감싼 객체이다. const coffee3 = { 'name': 'Americano3', 'price': 3000 }; // 객체 전체와 각 프로퍼티 값을 출력한다. console.log(coffee3); console.log(coffee3.name); console.log(coffee3.price); // 프로퍼티 이름으로 사용하고 싶은 문자열을 변수에 저장한다. const key1 = "name"; const key2 = "price"; // key1, key2라는 글자 자체가 프로퍼티 이름이 된다. const coffee4 = { key1: 'Americano4', key2: 3000 }; // 객체 전체와 여러 프로퍼티 접근 결과를 출력한다. console.log(coffee4); console.log(coffee4.name); console.log(coffee4.price); console.log(coffee4.key1); console.log(coffee4.key2); // 변수 key1, key2 안의 값을 프로퍼티 이름으로 사용한다. const coffee5 = {
[key1]: 'Americano5',
[key2]: 3000
};
``` 이 코드는 객체를 여러 방식으로 만든다. `coffee1`, `coffee2`, `coffee3`은 모두 `name`과 `price`라는 프로퍼티를 가진 객체이다. 차이는 프로퍼티 이름을 따옴표 없이 썼는지, 큰따옴표로 감쌌는지, 작은따옴표로 감쌌는지이다.// 객체 전체와 여러 프로퍼티 접근 결과를 출력한다. console.log(coffee5); console.log(coffee5.name); console.log(coffee5.price); console.log(coffee5.key1); console.log(coffee5.key2); </script>
그다음 `coffee4`와 `coffee5`는 더 중요한 비교이다. `coffee4`는 `key1`, `key2`라는 글자 자체를 프로퍼티 이름으로 사용한다. 반면 `coffee5`는 `[key1]`, `[key2]`처럼 대괄호를 사용해서 변수 안의 값을 프로퍼티 이름으로 사용한다.
객체 리터럴에서 `[변수명]`을 사용하면 변수 이름 자체가 아니라 변수 안에 들어 있는 값이 프로퍼티 이름이 된다.
### 예제 전체 코드 2 ```html // exam11.html ECMAScript6간단한 객체 생성
``` 이 코드는 같은 객체를 두 가지 방식으로 만든다. 첫 번째 `coffee` 객체는 `name: name`, `price: price`처럼 프로퍼티 이름과 변수 이름을 모두 작성한다. 두 번째 `coffee2` 객체는 `{ name, price }`처럼 짧게 작성한다.<script> // 객체에 넣을 값을 변수로 준비한다. const name = "Americano"; const price = 3000; // 변수 값을 프로퍼티 값으로 넣는 기본 객체 생성 방식이다. const coffee = { name: name, price: price }; // 객체와 객체의 key, value, entry를 출력한다. console.log(coffee); console.log(Object.keys(coffee)); console.log(Object.values(coffee)); console.log(Object.entries(coffee)); // 프로퍼티 이름과 변수 이름이 같을 때 사용하는 축약 표현이다. const coffee2 = { name, price }; // 축약 표현으로 만든 객체와 key, value, entry를 출력한다. console.log(coffee2); console.log(Object.keys(coffee2)); console.log(Object.values(coffee2)); console.log(Object.entries(coffee2)); </script>
이렇게 짧게 쓸 수 있는 이유는 프로퍼티 이름과 변수 이름이 같기 때문이다. `name`이라는 변수 값을 `name` 프로퍼티에 넣고, `price`라는 변수 값을 `price` 프로퍼티에 넣는 구조라면 한 번만 작성해도 된다.
마지막으로 `Object.keys()`, `Object.values()`, `Object.entries()`를 사용해서 객체 안의 이름, 값, 이름과 값의 쌍을 확인한다.
프로퍼티 이름과 변수 이름이 같으면 객체 리터럴에서 `name: name` 대신 `name`처럼 축약해서 작성할 수 있다.
### 이 예제에서 확인할 핵심 * 객체 리터럴은 `{}`로 객체를 만드는 문법이다. * 객체는 여러 프로퍼티를 하나로 묶어 표현한다. * 프로퍼티는 이름과 실제 값으로 구성된다. * 프로퍼티 이름은 따옴표 없이 작성할 수 있다. * 프로퍼티 이름을 큰따옴표나 작은따옴표로 감싸도 사용할 수 있다. * `key1: 값`은 `key1`이라는 글자 자체를 프로퍼티 이름으로 사용한다. * `[key1]: 값`은 변수 `key1` 안의 값을 프로퍼티 이름으로 사용한다. * `{ name, price }`는 `{ name: name, price: price }`를 짧게 쓴 표현이다. * `Object.keys()`는 객체의 프로퍼티 이름을 배열로 반환한다. * `Object.values()`는 객체의 프로퍼티 값을 배열로 반환한다. * `Object.entries()`는 프로퍼티 이름과 값을 한 쌍으로 묶은 배열을 반환한다.이 예제는 객체를 만들 때 프로퍼티 이름을 직접 쓰는 방식, 변수 값으로 만드는 방식, 축약해서 쓰는 방식을 비교한다.
프로퍼티 이름을 따옴표 없이 작성하기
// exam10.html // 프로퍼티 이름을 따옴표 없이 작성한 객체이다. const coffee1 = { name: "Americano1", price: 3000 }; console.log(coffee1); // 객체 전체를 출력한다. console.log(coffee1.name); // name 프로퍼티 값을 출력한다. console.log(coffee1.price); // price 프로퍼티 값을 출력한다.
coffee1은 객체 리터럴로 만든 객체이다.
중괄호 안에name과price프로퍼티가 들어 있다.
name: "Americano1"은name이라는 프로퍼티에"Americano1"값을 저장한다는 뜻이다.
price: 3000은price라는 프로퍼티에3000값을 저장한다는 뜻이다.
객체 안의 값을 꺼낼 때는 점 표기법을 사용할 수 있다.
점 표기법은객체이름.프로퍼티이름형태로 값을 꺼내는 방식이다.
그래서coffee1.name은"Americano1"을 꺼내고,coffee1.price는3000을 꺼낸다.// 출력결과 // { name: 'Americano1', price: 3000 } // Americano1 // 3000객체의 프로퍼티 값은
객체이름.프로퍼티이름형태로 꺼낼 수 있다.
프로퍼티 이름을 따옴표로 감싸기
// exam10.html // 프로퍼티 이름을 큰따옴표로 감싼 객체이다. const coffee2 = { "name": "Americano2", "price": 3000 }; console.log(coffee2); console.log(coffee2.name); console.log(coffee2.price); // 프로퍼티 이름을 작은따옴표로 감싼 객체이다. const coffee3 = { 'name': 'Americano3', 'price': 3000 }; console.log(coffee3); console.log(coffee3.name); console.log(coffee3.price);
coffee2는 프로퍼티 이름을 큰따옴표로 감쌌다.
coffee3은 프로퍼티 이름을 작은따옴표로 감쌌다.
하지만 결과적으로 만들어지는 프로퍼티 이름은name과price이다.
그래서coffee2.name,coffee2.price,coffee3.name,coffee3.price처럼 점 표기법으로 값을 꺼낼 수 있다.
출력 결과는 아래처럼 볼 수 있다.// 출력결과 // { name: 'Americano2', price: 3000 } // Americano2 // 3000 // { name: 'Americano3', price: 3000 } // Americano3 // 3000프로퍼티 이름이 일반적인 식별자 형태라면 따옴표 없이 작성해도 된다.
여기서 식별자는 변수 이름처럼 사용할 수 있는 이름이라고 이해하면 된다.
name과price처럼 일반적인 프로퍼티 이름은 따옴표 없이 작성해도 되고, 따옴표로 감싸도 된다.
변수 이름을 그대로 쓰면 프로퍼티 이름이 된다
// exam10.html // 프로퍼티 이름으로 사용하고 싶은 문자열을 변수에 저장한다. const key1 = "name"; const key2 = "price"; // key1, key2라는 글자 자체가 프로퍼티 이름이 된다. const coffee4 = { key1: 'Americano4', key2: 3000 }; console.log(coffee4); // 객체 전체를 출력한다. console.log(coffee4.name); // name 프로퍼티가 없으므로 undefined가 나온다. console.log(coffee4.price); // price 프로퍼티가 없으므로 undefined가 나온다. console.log(coffee4.key1); // key1 프로퍼티 값을 출력한다. console.log(coffee4.key2); // key2 프로퍼티 값을 출력한다.여기서 초보자가 가장 헷갈리기 쉽다.
key1변수에는"name"이 들어 있고,key2변수에는"price"가 들어 있다.
그래서coffee4의 프로퍼티 이름도name,price가 될 것처럼 보일 수 있다.
하지만 객체 리터럴에서key1: 'Americano4'라고 쓰면 변수key1안의 값을 사용하는 것이 아니다.
그냥key1이라는 글자 자체가 프로퍼티 이름이 된다.
그래서coffee4객체는 아래와 같은 구조가 된다.// exam10.html // 이해를 위한 구조 예시이다. const coffee4 = { key1: 'Americano4', key2: 3000 };이 객체에는
name프로퍼티와price프로퍼티가 없다.
대신key1프로퍼티와key2프로퍼티가 있다.
그래서 출력 결과는 아래처럼 된다.// 출력결과 // { key1: 'Americano4', key2: 3000 } // undefined // undefined // Americano4 // 3000
coffee4.name은 존재하지 않는 프로퍼티를 꺼내려고 했기 때문에undefined가 나온다.
coffee4.price도 마찬가지이다.
반대로coffee4.key1은 실제로 존재하는 프로퍼티이다.
그래서"Americano4"가 나온다.
coffee4.key2도 실제로 존재하므로3000이 나온다.
객체 리터럴에서key1: 값처럼 쓰면 변수key1의 값이 아니라key1이라는 이름 자체가 프로퍼티 이름이 된다.
변수 값을 프로퍼티 이름으로 사용하기
// exam10.html // 변수 key1, key2 안의 값을 프로퍼티 이름으로 사용한다. const coffee5 = {
};
console.log(coffee5); // 객체 전체를 출력한다.
console.log(coffee5.name); // name 프로퍼티 값을 출력한다.
console.log(coffee5.price); // price 프로퍼티 값을 출력한다.
console.log(coffee5.key1); // key1 프로퍼티가 없으므로 undefined가 나온다.
console.log(coffee5.key2); // key2 프로퍼티가 없으므로 undefined가 나온다.이번에는 `[key1]`, `[key2]`처럼 대괄호를 사용했다. 객체 리터럴에서 프로퍼티 이름 자리에 대괄호를 사용하면, 대괄호 안의 표현식을 계산한 결과가 프로퍼티 이름이 된다. <br/> 여기서 `key1` 변수의 값은 `"name"`이다. 그래서 `[key1]`은 `name` 프로퍼티가 된다. `key2` 변수의 값은 `"price"`이다. 그래서 `[key2]`는 `price` 프로퍼티가 된다. <br/> 결과적으로 `coffee5`는 아래와 같은 구조로 만들어진다. ```javascript // exam10.html // 이해를 위한 구조 예시이다. const coffee5 = { name: 'Americano5', price: 3000 };그래서
coffee5.name은"Americano5"를 출력하고,coffee5.price는3000을 출력한다.
반대로coffee5.key1,coffee5.key2는 존재하지 않으므로undefined가 나온다.
출력 결과는 아래와 같다.// 출력결과 // { name: 'Americano5', price: 3000 } // Americano5 // 3000 // undefined // undefined객체 리터럴에서
[key1]처럼 쓰면 변수key1안의 값이 프로퍼티 이름으로 사용된다.
이 문법을 계산된 프로퍼티 이름이라고 부를 수 있다.
프로퍼티 축약 표현 사용하기
// exam11.html // 객체에 넣을 값을 변수로 준비한다. const name = "Americano"; const price = 3000; // 변수 값을 프로퍼티 값으로 넣는 기본 객체 생성 방식이다. const coffee = { name: name, price: price }; console.log(coffee);이 코드는
name변수와price변수를 사용해서coffee객체를 만든다.
name: name은name이라는 프로퍼티에name변수의 값을 넣는다는 뜻이다.
price: price는price라는 프로퍼티에price변수의 값을 넣는다는 뜻이다.
현재name변수에는"Americano"가 들어 있다.
price변수에는3000이 들어 있다.
그래서coffee객체는{ name: "Americano", price: 3000 }형태가 된다.
출력 결과는 아래와 같다.// 출력결과 // { name: 'Americano', price: 3000 }이 방식은 정확하지만 같은 이름을 두 번 반복해서 적어야 한다.
name: name,price: price처럼 왼쪽과 오른쪽 이름이 같기 때문이다.
프로퍼티 이름과 변수 이름이 같으면 더 짧게 작성할 수 있다.
이름이 같을 때 더 짧게 객체 만들기
// exam11.html // 프로퍼티 이름과 변수 이름이 같을 때 사용하는 축약 표현이다. const coffee2 = { name, price }; console.log(coffee2);
const coffee2 = { name, price }는 짧아 보이지만, 실제 의미는 아래와 같다.// exam11.html // 이해를 위한 구조 예시이다. const coffee2 = { name: name, price: price };즉,
name이라고 한 번만 쓰면name프로퍼티에name변수 값을 넣는다.
price라고 한 번만 쓰면price프로퍼티에price변수 값을 넣는다.
이렇게 작성할 수 있는 이유는 프로퍼티 이름과 변수 이름이 같기 때문이다.
둘이 다르면 이런 축약 표현을 그대로 사용할 수 없다.
출력 결과는 아래와 같다.// 출력결과 // { name: 'Americano', price: 3000 }
coffee와coffee2는 작성 방식만 다를 뿐 같은 구조의 객체를 만든다.
객체 리터럴에서 프로퍼티 이름과 변수 이름이 같으면 한 번만 써서 객체를 만들 수 있다.
Object.keys로 프로퍼티 이름 확인하기
// exam11.html // 객체의 프로퍼티 이름들을 배열로 반환한다. console.log(Object.keys(coffee)); console.log(Object.keys(coffee2));
Object.keys()는 객체 안의 프로퍼티 이름만 배열로 꺼낸다.
여기서 프로퍼티 이름은name과price이다.
coffee객체도name,price를 가지고 있고,coffee2객체도name,price를 가지고 있다.
그래서 두 출력 결과는 같다.// 출력결과 // ['name', 'price'] // ['name', 'price']
Object.keys()는 객체에서 프로퍼티 이름만 확인하고 싶을 때 사용한다.
Object.values로 프로퍼티 값 확인하기
// exam11.html // 객체의 프로퍼티 값들을 배열로 반환한다. console.log(Object.values(coffee)); console.log(Object.values(coffee2));
Object.values()는 객체 안의 프로퍼티 값만 배열로 꺼낸다.
여기서 프로퍼티 값은"Americano"와3000이다.
coffee와coffee2는 같은 구조의 객체이므로 값도 같다.
그래서 출력 결과는 아래와 같다.// 출력결과 // ['Americano', 3000] // ['Americano', 3000]프로퍼티 이름이 아니라 실제 값만 보고 싶을 때는
Object.values()를 사용한다.
Object.entries로 프로퍼티 이름과 값 함께 확인하기
// exam11.html // 객체의 프로퍼티 이름과 값을 한 쌍으로 묶어 배열로 반환한다. console.log(Object.entries(coffee)); console.log(Object.entries(coffee2));
Object.entries()는 객체의 프로퍼티 이름과 값을 한 쌍으로 묶어 배열로 꺼낸다.
즉,name과"Americano"가 한 쌍이고,price와3000이 한 쌍이다.
출력 결과는 아래처럼 볼 수 있다.// 출력결과 // [['name', 'Americano'], ['price', 3000]] // [['name', 'Americano'], ['price', 3000]]
Object.entries()는 객체 안의 이름과 값을 함께 확인해야 할 때 유용하다.
앞에서 본Map의entries()처럼 이름과 값을 묶어서 본다고 이해하면 된다.
Object.entries()는 객체의 프로퍼티 이름과 값을 한 쌍으로 확인할 때 사용한다.
객체 리터럴 확장 문법 정리하기
이 예제에서 가장 헷갈리기 쉬운 부분은
key1과[key1]의 차이이다.
둘은 비슷해 보이지만 결과가 완전히 다르다.
key1: 값이라고 쓰면key1이라는 글자 자체가 프로퍼티 이름이 된다.
반면[key1]: 값이라고 쓰면 변수key1안에 들어 있는 값이 프로퍼티 이름이 된다.
또{ name, price }는 객체를 짧게 만드는 문법이다.
단, 프로퍼티 이름과 변수 이름이 같을 때만 이 축약 표현을 사용할 수 있다.
정리하면 아래처럼 볼 수 있다.
name: "Americano"는name프로퍼티에 값을 저장한다."name": "Americano"처럼 따옴표로 감싸도 프로퍼티 이름은name이다.key1: "Americano"는key1이라는 프로퍼티를 만든다.[key1]: "Americano"는 변수key1의 값인name을 프로퍼티 이름으로 사용한다.name: name은name프로퍼티에name변수 값을 넣는다.{ name }은name: name을 짧게 쓴 표현이다.객체 리터럴에서는 프로퍼티 이름을 직접 쓸 수도 있고, 변수 값을 계산해서 프로퍼티 이름으로 사용할 수도 있다.
핵심 정리
이 예제의 핵심은 객체 리터럴에서 프로퍼티 이름과 값을 연결하는 방식이다.
객체는 관련 있는 값을 하나로 묶고, 프로퍼티 이름을 통해 값을 꺼낸다.
이 흐름은 아래처럼 정리할 수 있다.
- 객체 리터럴은
{}로 객체를 만드는 문법이다.- 객체는 여러 프로퍼티를 하나로 묶어 표현한다.
- 프로퍼티는 이름과 값으로 구성된다.
coffee1.name은coffee1객체의name값을 꺼낸다.- 프로퍼티 이름은 따옴표 없이 써도 되고, 따옴표로 감싸도 된다.
key1: 값은key1이라는 글자 자체를 프로퍼티 이름으로 사용한다.[key1]: 값은 변수key1안의 값을 프로퍼티 이름으로 사용한다.{ name, price }는{ name: name, price: price }를 짧게 쓴 표현이다.Object.keys()는 객체의 프로퍼티 이름을 배열로 반환한다.Object.values()는 객체의 프로퍼티 값을 배열로 반환한다.Object.entries()는 프로퍼티 이름과 값을 한 쌍으로 묶은 배열을 반환한다.따라서 이 예제가 알려 주는 핵심은 이것이다.
객체 리터럴 확장 문법은 객체를 만들 때 프로퍼티 이름을 더 유연하게 만들고, 변수 이름과 프로퍼티 이름이 같을 때 더 짧게 작성할 수 있게 해 주는 문법이다.
이 예제는 같은 기능을 선언적 함수, 함수 표현식, 화살표 함수로 각각 작성하고 버튼 클릭으로 실행하는 흐름을 보여 준다.
또 이미지에 마우스를 올렸을 때와 뗐을 때opacity값을 바꾸는event처리도 함께 보여 준다.
이 예제는 단순히 함수 문법만 보는 코드가 아니다.
함수를 만들어 두고, 사용자의 동작이 발생했을 때 그 함수를 실행하는 구조까지 확인하는 예제이다.
이 예제의 핵심은 함수 정의 방식과event연결 방식을 함께 이해하는 것이다.
함수 정의 방식이란 무엇인가
함수 정의 방식은 함수를 어떤 문법으로 만들 것인지에 대한 방식이다.
함수는 특정 동작을 이름이나 변수에 묶어 두고, 필요할 때 호출해서 실행하는 코드 묶음이다.
이 예제에서는 같은 덧셈 동작을 세 가지 방식으로 만든다.
- 선언적 함수
- 함수 표현식
- 화살표 함수
선언적 함수는
function f1(...) { ... }처럼 함수 이름을 직접 붙여 만드는 방식이다.
함수 표현식은 이름 없는 익명 함수를 변수에 저장하는 방식이다.
화살표 함수는=>기호를 사용해서 함수를 더 짧게 작성하는 방식이다.
함수 정의 방식은 다르지만, 함수가 호출되어 실행된다는 큰 흐름은 같다.
이벤트란 무엇인가
event는 사용자가 웹 페이지에서 어떤 동작을 했을 때 발생하는 신호이다.
예를 들어 버튼을 클릭하거나, 이미지 위에 마우스를 올리거나, 입력창에 글자를 입력하는 동작이 모두event가 될 수 있다.
이 예제에서는 두 종류의event를 사용한다.
- 버튼을 클릭했을 때 발생하는
click계열event- 이미지 위에 마우스를 올리거나 뗐을 때 발생하는
mouseover,mouseoutevent
event가 발생하면 그때 실행할 함수를 연결할 수 있다.
이렇게event가 발생했을 때 실행되는 함수를event handler또는event listener라고 이해하면 된다.
event처리는 사용자의 동작에 맞춰 코드가 실행되게 만드는 흐름이다.
예제 전체 코드
// exam12.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>Insert title here</title> <style> img { /* 이미지를 처음에는 흐리게 보이게 한다. */ opacity: 0.3; /* opacity 값이 바뀔 때 1초 동안 부드럽게 변하게 한다. */ transition: opacity 1s; } </style> </head> <body> <h2>다양한 함수 정의 방법과 응용</h2> <hr /> <!-- 선언적 함수 f1을 클릭 이벤트로 호출한다. --> <button onClick="f1(10, 20);">선언적 함수 호출</button> <!-- 함수 표현식으로 만든 f2를 클릭 이벤트로 호출한다. --> <button onCLICK="f2(100, 200);">함수표현식 함수 호출</button> <!-- 화살표 함수로 만든 f3를 클릭 이벤트로 호출한다. --> <button onclick="f3(1000, 2000);">화살표 함수 호출</button> <!-- 외부 보조 함수 파일을 불러온다. 이 예제의 핵심 동작에는 직접 사용되지 않는다. --> <script src="../util.js"></script> <script> // 1. 선언적 함수 // 함수 이름 f1을 직접 붙여 함수를 만든다. function f1(p1, p2) { // 전달받은 두 값을 더해서 알림창으로 보여 준다. alert(p1 + p2); } // 2. 함수 표현식 // 이름 없는 익명 함수를 변수 f2에 저장한다. let f2 = function (p1, p2) { // 전달받은 두 값을 더해서 알림창으로 보여 준다. alert(p1 + p2); }; // 3. 화살표 함수 // function 키워드 대신 => 기호를 사용한다. let f3 = (p1, p2) => alert(p1 + p2); </script> <hr /> <!-- 마우스 이벤트를 적용할 이미지이다. --> <img src="https://i.namu.wiki/i/HehKgZCWtYmx88aflvkUl29biDUxoTVPPpWOBaakkKZEjvGP6T5vGZAYOUip8UhwX9Wi-hDo-O4i1wBc9VNtIQ.webp" /> <script> // 문서에서 img 태그를 하나 찾아 domObj 변수에 저장한다. const domObj = document.querySelector("img"); // 이미지 위에 마우스를 올리면 실행된다. domObj.addEventListener("mouseover", e => e.target.style.opacity = 1.0); // 이미지에서 마우스를 떼면 실행된다. domObj.addEventListener("mouseout", e => e.target.style.opacity = 0.3); </script> </body> </html>이 코드는 크게 두 부분으로 나누어 볼 수 있다.
첫 번째는 버튼을 클릭했을 때 세 가지 방식으로 만든 함수를 실행하는 부분이다.
두 번째는 이미지에 마우스를 올리거나 뗐을 때 이미지의 투명도를 바꾸는 부분이다.
위쪽 버튼 예제에서는f1,f2,f3가 모두 두 숫자를 더해서alert()로 보여 준다.
하지만 함수를 만든 방식이 다르다.
아래쪽 이미지 예제에서는document.querySelector()로 이미지를 찾고,addEventListener()로mouseover와mouseoutevent를 연결한다.
event가 발생하면 화살표 함수가 실행되고, 이미지의opacity값이 바뀐다.
이 예제는 함수를 만드는 문법과, 그 함수를event에 연결해서 실행하는 흐름을 함께 보여 준다.
이 예제에서 확인할 핵심
- 선언적 함수는 함수 이름을 직접 붙여 만든다.
- 함수 표현식은 익명 함수를 변수에 저장해서 만든다.
- 화살표 함수는
=>기호로 함수를 짧게 만든다.button의onclick속성은 버튼 클릭 시 실행할 코드를 연결한다.alert()는 알림창을 띄워 값을 보여 준다.document.querySelector("img")는 문서에서 첫 번째img요소를 찾는다.addEventListener()는 특정event가 발생했을 때 실행할 함수를 연결한다.mouseover는 마우스가 요소 위로 올라갔을 때 발생한다.mouseout은 마우스가 요소 밖으로 나갔을 때 발생한다.e.target은event가 실제로 발생한 대상을 가리킨다.style.opacity는 요소의 투명도를 바꾼다.이 예제는 함수 문법을 버튼 클릭과 이미지 마우스
event에 실제로 연결해 보는 예제이다.
선언적 함수로 버튼 클릭 처리하기
// exam12.html <!-- 선언적 함수 f1을 클릭 이벤트로 호출한다. --> <button onClick="f1(10, 20);">선언적 함수 호출</button> <script> // 함수 이름 f1을 직접 붙여 함수를 만든다. function f1(p1, p2) { // 전달받은 두 값을 더해서 알림창으로 보여 준다. alert(p1 + p2); } </script>선언적 함수는 함수 이름을 직접 붙여 만드는 방식이다.
여기서는function f1(p1, p2) { ... }형태로f1함수를 만들었다.
p1과p2는 매개변수이다.
매개변수는 함수가 호출될 때 전달받은 값을 담는 자리이다.
버튼에는onClick="f1(10, 20);"이 적혀 있다.
이 버튼을 클릭하면f1(10, 20)이 실행된다.
그러면p1에는10,p2에는20이 들어간다.
alert(p1 + p2)는 두 값을 더한 결과를 알림창으로 보여 준다.
따라서 버튼을 클릭하면30이 알림창에 출력된다.// 출력결과 // 30선언적 함수는 함수 이름을 직접 붙여 두고, 그 이름으로 호출하는 방식이다.
함수 표현식으로 버튼 클릭 처리하기
// exam12.html <!-- 함수 표현식으로 만든 f2를 클릭 이벤트로 호출한다. --> <button onCLICK="f2(100, 200);">함수표현식 함수 호출</button> <script> // 이름 없는 익명 함수를 변수 f2에 저장한다. let f2 = function (p1, p2) { // 전달받은 두 값을 더해서 알림창으로 보여 준다. alert(p1 + p2); }; </script>이 코드는 함수 표현식이다.
함수 표현식은 함수를 값처럼 만들어 변수에 저장하는 방식이다.
여기서는function (p1, p2) { ... }처럼 함수 이름이 없는 익명 함수를 만들었다.
그리고 그 익명 함수를 변수f2에 저장했다.
버튼에는onCLICK="f2(100, 200);"이 적혀 있다.
이 버튼을 클릭하면 변수f2에 저장된 함수가 실행된다.
그러면p1에는100,p2에는200이 들어간다.
alert(p1 + p2)가 실행되므로 알림창에는300이 출력된다.// 출력결과 // 300여기서
onCLICK처럼 대문자가 섞여 있어도HTML속성 이름은 대소문자를 엄격하게 구분하지 않기 때문에 클릭 이벤트로 동작할 수 있다.
하지만 코드 가독성을 위해 보통은onclick처럼 소문자로 통일해서 작성하는 것이 좋다.
let f2 = function (...) { ... }는 익명 함수를 변수f2에 저장한 함수 표현식이다.
화살표 함수로 버튼 클릭 처리하기
// exam12.html <!-- 화살표 함수로 만든 f3를 클릭 이벤트로 호출한다. --> <button onclick="f3(1000, 2000);">화살표 함수 호출</button> <script> // function 키워드 대신 => 기호를 사용한다. let f3 = (p1, p2) => alert(p1 + p2); </script>이 코드는 화살표 함수이다.
화살표 함수는function키워드 대신=>기호를 사용해서 함수를 만드는 방식이다.
let f3 = (p1, p2) => alert(p1 + p2);는p1과p2를 받아 더한 뒤alert()로 보여 주는 함수를f3변수에 저장한다.
버튼에는onclick="f3(1000, 2000);"이 적혀 있다.
이 버튼을 클릭하면f3(1000, 2000)이 실행된다.
그러면p1에는1000,p2에는2000이 들어간다.
따라서 알림창에는3000이 출력된다.// 출력결과 // 3000이 함수는 실행할 코드가
alert(p1 + p2)하나뿐이다.
그래서 중괄호 없이 한 줄로 작성했다.
간단한 동작 하나를 처리하는 함수라면 화살표 함수로 짧게 작성할 수 있다.
세 가지 함수 정의 방식 비교하기
이 예제의 위쪽 버튼 세 개는 모두 숫자 두 개를 더해서 알림창으로 보여 준다.
다만 함수를 만든 방식이 다르다.// exam12.html // 1. 선언적 함수 function f1(p1, p2) { alert(p1 + p2); } // 2. 함수 표현식 let f2 = function (p1, p2) { alert(p1 + p2); }; // 3. 화살표 함수 let f3 = (p1, p2) => alert(p1 + p2);세 함수는 모두
p1과p2를 받아 더한다.
하지만 작성 방식은 다르다.
선언적 함수는function f1(...)처럼 함수 이름이 직접 붙어 있다.
함수 표현식은 이름 없는 함수를 변수f2에 저장한다.
화살표 함수는=>를 사용해서 더 짧게 작성한다.
정리하면 아래처럼 볼 수 있다.
f1은 선언적 함수이다.f2는 익명 함수를 변수에 저장한 함수 표현식이다.f3는 화살표 함수이다.- 세 함수 모두 버튼 클릭 시 실행된다.
- 세 함수 모두 전달받은 두 값을 더해
alert()로 보여 준다.함수 정의 방식은 달라도,
event에 연결된 함수가 호출되면 그 안의 코드가 실행된다는 흐름은 같다.
CSS로 이미지의 기본 투명도 정하기
// exam12.html img { /* 이미지를 처음에는 흐리게 보이게 한다. */ opacity: 0.3; /* opacity 값이 바뀔 때 1초 동안 부드럽게 변하게 한다. */ transition: opacity 1s; }이 부분은 이미지의 기본 스타일을 정한다.
opacity는 요소의 투명도를 의미한다.
값이1이면 완전히 보이고,0에 가까워질수록 투명해진다.
여기서는opacity: 0.3;을 사용했다.
그래서 이미지는 처음에 흐리게 보인다.
transition: opacity 1s;는opacity값이 바뀔 때 바로 바뀌지 않고1초 동안 부드럽게 변하도록 만든다.
즉, 마우스를 올렸을 때 이미지가 갑자기 진해지는 것이 아니라 자연스럽게 진해진다.
opacity는 요소의 투명도이고,transition은 값이 바뀔 때 변화를 부드럽게 만들어 주는 속성이다.
querySelector로 이미지 찾기
// exam12.html // 문서에서 img 태그를 하나 찾아 domObj 변수에 저장한다. const domObj = document.querySelector("img");
document는 현재 웹 문서 전체를 의미한다.
쉽게 말하면 지금 브라우저에 열려 있는HTML페이지라고 이해하면 된다.
querySelector("img")는 문서 안에서 조건에 맞는 요소를 하나 찾는 메서드이다.
여기서는"img"를 전달했으므로 문서 안의 첫 번째img태그를 찾는다.
찾은 이미지 요소는domObj변수에 저장된다.
이제domObj를 사용해서 이미지에event를 연결하거나, 이미지의style을 바꿀 수 있다.
document.querySelector("img")는 문서에서 첫 번째img요소를 찾아 변수에 저장하는 코드이다.
addEventListener로 mouseover 이벤트 연결하기
// exam12.html // 이미지 위에 마우스를 올리면 실행된다. domObj.addEventListener("mouseover", e => e.target.style.opacity = 1.0);
addEventListener()는 특정 요소에event를 연결하는 메서드이다.
여기서는domObj, 즉 이미지에mouseoverevent를 연결했다.
mouseover는 마우스가 요소 위로 올라갔을 때 발생하는event이다.
따라서 사용자가 이미지 위에 마우스를 올리면 뒤쪽의 화살표 함수가 실행된다.
e는event객체이다.
event객체는event가 발생했을 때 관련 정보를 담고 있는 객체이다.
여기서는 어떤 요소에서event가 발생했는지 알기 위해e.target을 사용한다.
e.target은event가 실제로 발생한 대상을 가리킨다.
이 경우event가 이미지에서 발생했으므로e.target은 이미지 요소이다.
e.target.style.opacity = 1.0은 이미지의 투명도를1.0으로 바꾼다.
opacity가1.0이면 이미지가 선명하게 보인다.
마우스를 이미지 위에 올리면mouseoverevent가 발생하고, 이미지의opacity가1.0으로 바뀐다.
addEventListener로 mouseout 이벤트 연결하기
// exam12.html // 이미지에서 마우스를 떼면 실행된다. domObj.addEventListener("mouseout", e => e.target.style.opacity = 0.3);이번에는
mouseoutevent를 연결했다.
mouseout은 마우스가 요소 밖으로 나갔을 때 발생하는event이다.
사용자가 이미지 위에 올려 둔 마우스를 이미지 밖으로 이동하면 화살표 함수가 실행된다.
그리고e.target.style.opacity = 0.3이 실행된다.
그러면 이미지의 투명도가 다시0.3으로 바뀐다.
처음CSS에서 지정한 흐린 상태로 돌아가는 것이다.
출력 결과처럼 콘솔에 값이 찍히지는 않지만, 브라우저 화면에서 이미지가 흐려졌다가 진해졌다가 다시 흐려지는 변화를 볼 수 있다.
마우스가 이미지 밖으로 나가면mouseoutevent가 발생하고, 이미지의opacity가 다시0.3으로 바뀐다.
이벤트 처리 흐름 정리하기
이 예제의
event처리 흐름은 두 부분으로 나누어 볼 수 있다.
버튼event는onclick속성으로 함수 호출을 직접 연결했다.
이미지event는addEventListener()로event이름과 실행할 함수를 연결했다.
버튼 처리 흐름은 아래와 같다.
- 사용자가 첫 번째 버튼을 클릭한다.
f1(10, 20)이 실행된다.- 알림창에
30이 출력된다.- 사용자가 두 번째 버튼을 클릭한다.
f2(100, 200)이 실행된다.- 알림창에
300이 출력된다.- 사용자가 세 번째 버튼을 클릭한다.
f3(1000, 2000)이 실행된다.- 알림창에
3000이 출력된다.이미지 처리 흐름은 아래와 같다.
- 처음 이미지는
opacity: 0.3으로 흐리게 보인다.- 이미지 위에 마우스를 올린다.
mouseoverevent가 발생한다.- 이미지의
opacity가1.0으로 바뀐다.- 이미지 밖으로 마우스를 이동한다.
mouseoutevent가 발생한다.- 이미지의
opacity가0.3으로 돌아간다.
event처리는 사용자의 동작이 발생했을 때 미리 연결해 둔 함수를 실행하는 흐름이다.
onclick과 addEventListener 비교하기
이 예제에는
event를 연결하는 방식이 두 가지 나온다.
첫 번째는HTML태그에onclick속성을 직접 작성하는 방식이다.
두 번째는JavaScript코드에서addEventListener()를 사용하는 방식이다.
onclick방식은 태그 안에서 바로 어떤 함수를 실행할지 볼 수 있어서 단순하다.
하지만HTML과JavaScript코드가 섞인다.
addEventListener()방식은JavaScript코드에서event를 따로 연결한다.
그래서 화면 구조와 동작 코드를 분리해서 관리하기 좋다.
이 예제에서는 두 방식을 모두 보여 주지만, 이미지event에서는addEventListener()를 사용한다.
이 방식은 특정 요소를 먼저 찾고, 그 요소에event를 연결하는 흐름이다.
정리하면 아래처럼 볼 수 있다.
onclick은HTML속성에 직접event코드를 작성한다.addEventListener()는JavaScript에서event를 연결한다.onclick은 간단한 예제에서 흐름을 보기 쉽다.addEventListener()는event연결을 코드로 관리하기 좋다.
onclick은 태그 안에 직접 연결하는 방식이고,addEventListener()는JavaScript에서 요소를 찾아event를 연결하는 방식이다.
핵심 정리
이 예제의 핵심은 함수 정의 방식과
event처리 흐름이다.
같은 덧셈 기능도 선언적 함수, 함수 표현식, 화살표 함수로 각각 만들 수 있다.
그리고 만든 함수는 버튼 클릭 같은event와 연결해서 실행할 수 있다.
이 흐름은 아래처럼 정리할 수 있다.
- 선언적 함수는
function f1(...) { ... }처럼 함수 이름을 직접 붙여 만든다.- 함수 표현식은 익명 함수를 변수에 저장해서 만든다.
- 화살표 함수는
=>기호를 사용해서 함수를 짧게 만든다.- 버튼의
onclick속성은 클릭 시 실행할 함수를 연결한다.f1(10, 20)은30을 알림창으로 보여 준다.f2(100, 200)은300을 알림창으로 보여 준다.f3(1000, 2000)은3000을 알림창으로 보여 준다.document.querySelector("img")는 문서에서 이미지 요소를 찾는다.addEventListener("mouseover", ...)는 마우스를 올렸을 때 실행할 함수를 연결한다.addEventListener("mouseout", ...)는 마우스를 뗐을 때 실행할 함수를 연결한다.e.target은event가 발생한 실제 대상을 가리킨다.style.opacity를 바꾸면 이미지의 투명도를 바꿀 수 있다.따라서 이 예제가 알려 주는 핵심은 이것이다.
함수는 여러 방식으로 정의할 수 있고,event와 연결하면 사용자의 클릭이나 마우스 움직임에 맞춰 실행되도록 만들 수 있다.
이 예제는
JavaScript에서class를 사용해 객체를 만드는 기본 흐름을 보여 준다.
class는 객체를 만들기 위한 틀이다.
객체는 실제 데이터를 가진 하나의 대상이다.
예를 들어 학생 한 명을 코드로 표현하려면 이름, 국어 점수, 수학 점수, 영어 점수, 과학 점수가 필요하다.
이 값들을 매번 따로따로 관리하면 복잡하다.
그래서Student라는class를 만들고, 그 안에 학생 한 명이 가져야 할 값을 정리한다.
이 예제의 핵심은class라는 틀을 만들고,new로 실제 학생 객체를 생성하는 흐름을 이해하는 것이다.
클래스란 무엇인가
class는 객체를 만들기 위한 설계도이다.
초보자 기준에서는 “이런 모양의 객체를 만들겠다”라고 정해 두는 틀이라고 이해하면 된다.
학생 객체를 만들려면 학생마다 이름과 점수가 필요하다.
Studentclass는 학생 객체가 어떤 값을 가져야 하는지 정해 둔다.
이 예제의Student객체는 아래 값을 가진다.
- 이름
- 국어 점수
- 수학 점수
- 영어 점수
- 과학 점수
이 값들은 학생마다 다르다.
둘리는 둘리의 점수를 가지고, 또치는 또치의 점수를 가진다.
그래서class는 하나만 만들고, 실제 학생 객체는 여러 개 만들 수 있다.
class는 객체 하나를 직접 의미하는 것이 아니라, 같은 구조의 객체를 여러 개 만들기 위한 틀이다.
객체와 인스턴스란 무엇인가
객체는 실제 값을 가진 데이터 묶음이다.
예를 들어 둘리 학생 객체는 이름이"둘리"이고, 국어 점수가96, 수학 점수가98인 실제 데이터이다.
instance는class를 바탕으로 실제로 만들어진 객체를 말한다.
즉,Studentclass로 만든st1,st2,st3는 모두Student의instance라고 볼 수 있다.
쉽게 구분하면 아래와 같다.
class는 객체를 만들기 위한 틀이다.- 객체는 실제 데이터를 가진 대상이다.
instance는 특정class로부터 만들어진 객체이다.
Student는 틀이고,st1,st2,st3는 그 틀로 만든 실제 학생 객체이다.
예제 전체 코드
// exam13.html <!doctype html> <html> <head> <meta charset="UTF-8" /> <title>Insert title here</title> </head> <body> <h1>자바스크립트의 객체 생성과 사용(클래스)</h1> <hr /> <!-- 화면에 내용을 출력하기 위한 보조 함수 파일을 불러온다. --> <script src="./util.js"></script> <script> // Student 클래스는 학생 객체를 만들기 위한 틀이다. class Student { // constructor는 new Student(...)가 실행될 때 자동으로 호출된다. constructor(name, korean, math, english, science) { // 전달받은 name 값을 현재 객체의 이름 프로퍼티에 저장한다. this.이름 = name; // 전달받은 korean 값을 현재 객체의 국어 프로퍼티에 저장한다. this.국어 = korean; // 전달받은 math 값을 현재 객체의 수학 프로퍼티에 저장한다. this.수학 = math; // 전달받은 english 값을 현재 객체의 영어 프로퍼티에 저장한다. this.영어 = english; // 전달받은 science 값을 현재 객체의 과학 프로퍼티에 저장한다. this.과학 = science; } // 객체를 문자열로 표현할 때 사용할 메서드이다. toString() { // 현재 객체가 가진 이름과 점수를 하나의 문자열로 만들어 반환한다. return ( this.이름 + " : " + this.국어 + ", " + this.수학 + ", " + this.영어 + ", " + this.과학 ); } } // Student 클래스로 첫 번째 학생 객체를 만든다. var st1 = new Student("둘리", 96, 98, 92, 98); // Student 클래스로 두 번째 학생 객체를 만든다. var st2 = new Student("또치", 92, 98, 96, 98); // Student 클래스로 세 번째 학생 객체를 만든다. var st3 = new Student("도우너", 76, 96, 94, 90); // st1 객체를 h2 태그로 화면에 출력한다. write(st1, "h2"); // st2 객체를 h2 태그로 화면에 출력한다. write(st2, "h2"); // st3 객체를 h2 태그로 화면에 출력한다. write(st3, "h2"); </script> </body> </html>이 코드는 크게 네 부분으로 나누어 볼 수 있다.
첫 번째는Studentclass를 정의하는 부분이다.
두 번째는constructor에서 전달받은 값을 객체 안에 저장하는 부분이다.
세 번째는toString()으로 객체를 문자열 형태로 바꾸는 부분이다.
네 번째는new Student(...)로 실제 학생 객체를 만들고write()로 화면에 출력하는 부분이다.
이 예제에서 중요한 흐름은class작성에서 끝나지 않는다.
class는 객체를 만들기 위한 틀일 뿐이다.
실제로 객체가 만들어지는 시점은new Student(...)가 실행될 때이다.
class를 정의하는 것은 틀을 만드는 것이고,new를 사용하는 것은 그 틀로 실제 객체를 만드는 것이다.
이 예제에서 확인할 핵심
class는 객체를 만들기 위한 틀이다.Student는 학생 객체를 만들기 위한class이다.constructor는 객체가 생성될 때 자동으로 실행되는 특별한 메서드이다.new Student(...)를 실행하면constructor가 호출된다.this는 현재 만들어지고 있는 객체를 가리킨다.this.이름 = name은 현재 객체의 이름 프로퍼티에name값을 저장한다.toString()은 객체를 문자열로 표현할 때 사용할 수 있는 메서드이다.st1,st2,st3는Studentclass로 만든 객체이다.write()는 화면에 내용을 출력하기 위해 사용하는 보조 함수이다.이 예제는
class를 정의하고, 생성자로 값을 저장하고, 메서드로 객체 내용을 문자열로 만들어 출력하는 흐름을 보여 준다.
Student 클래스 만들기
// exam13.html // Student 클래스는 학생 객체를 만들기 위한 틀이다. class Student { // constructor는 new Student(...)가 실행될 때 자동으로 호출된다. constructor(name, korean, math, english, science) { this.이름 = name; this.국어 = korean; this.수학 = math; this.영어 = english; this.과학 = science; } // 객체를 문자열로 표현할 때 사용할 메서드이다. toString() { return ( this.이름 + " : " + this.국어 + ", " + this.수학 + ", " + this.영어 + ", " + this.과학 ); } }
class Student는Student라는 이름의 클래스를 만든다는 뜻이다.
이class는 학생 객체를 만들기 위한 틀이다.
Student안에는constructor와toString()이 들어 있다.
constructor는 객체가 만들어질 때 값을 저장하는 역할을 한다.
toString()은 객체를 문자열로 표현하는 역할을 한다.
여기서 중요한 점은class를 정의했다고 해서 학생 객체가 바로 만들어지는 것은 아니라는 점이다.
class는 틀만 만든 상태이다.
실제 객체는 뒤에서new Student(...)를 실행해야 만들어진다.
class Student { ... }는 학생 객체를 만들기 위한 구조를 미리 정해 두는 코드이다.
constructor는 객체가 만들어질 때 실행된다
// exam13.html constructor(name, korean, math, english, science) { // 전달받은 name 값을 현재 객체의 이름 프로퍼티에 저장한다. this.이름 = name; // 전달받은 korean 값을 현재 객체의 국어 프로퍼티에 저장한다. this.국어 = korean; // 전달받은 math 값을 현재 객체의 수학 프로퍼티에 저장한다. this.수학 = math; // 전달받은 english 값을 현재 객체의 영어 프로퍼티에 저장한다. this.영어 = english; // 전달받은 science 값을 현재 객체의 과학 프로퍼티에 저장한다. this.과학 = science; }
constructor는 생성자라고 부른다.
생성자는 객체가 만들어질 때 자동으로 실행되는 특별한 메서드이다.
new Student("둘리", 96, 98, 92, 98)가 실행되면constructor가 자동으로 호출된다.
그리고"둘리",96,98,92,98값이 차례대로 매개변수에 들어간다.
값이 들어가는 흐름은 아래와 같다.
name에는"둘리"가 들어간다.korean에는96이 들어간다.math에는98이 들어간다.english에는92가 들어간다.science에는98이 들어간다.그다음
this.이름 = name처럼 현재 객체 안에 값을 저장한다.
이 과정을 통해 객체는 자기만의 이름과 점수를 가지게 된다.
constructor는 객체를 만들 때 전달된 값을 현재 객체 안에 저장하는 역할을 한다.
this는 현재 만들어지는 객체를 가리킨다
this는 현재 코드가 어떤 객체를 기준으로 실행되는지 가리키는 값이다.
이 예제에서는constructor가 실행될 때this가 지금 만들어지는 학생 객체를 가리킨다.
예를 들어 아래 코드가 실행된다고 보자.// exam13.html var st1 = new Student("둘리", 96, 98, 92, 98);이 순간
Student객체 하나가 새로 만들어진다.
그리고constructor안에서this는 바로 그 새로 만들어지는 객체를 가리킨다.
그래서 아래 코드는 새로 만들어지는st1객체 안에 값을 저장하는 흐름이 된다.// exam13.html // st1 객체의 이름 프로퍼티에 둘리를 저장하는 흐름으로 이해하면 된다. this.이름 = name; this.국어 = korean; this.수학 = math; this.영어 = english; this.과학 = science;이 코드가 실행된 뒤
st1은 아래와 같은 값을 가진 객체가 된다.// exam13.html // 이해를 위한 구조 예시이다. st1 = { 이름: "둘리", 국어: 96, 수학: 98, 영어: 92, 과학: 98 };실제 코드에서 직접 이렇게 객체를 만든 것은 아니다.
하지만constructor가 실행된 결과를 이해하기 쉽게 표현하면 이런 구조가 된다.
this.프로퍼티 = 값은 현재 만들어지는 객체 안에 값을 저장하는 코드이다.
new로 학생 객체 만들기
// exam13.html // Student 클래스로 첫 번째 학생 객체를 만든다. var st1 = new Student("둘리", 96, 98, 92, 98); // Student 클래스로 두 번째 학생 객체를 만든다. var st2 = new Student("또치", 92, 98, 96, 98); // Student 클래스로 세 번째 학생 객체를 만든다. var st3 = new Student("도우너", 76, 96, 94, 90);
new Student(...)는Studentclass를 바탕으로 새 객체를 만드는 코드이다.
new는 새로운 객체를 만들 때 사용하는 키워드이다.
st1,st2,st3는 각각 다른 객체이다.
같은Studentclass로 만들었지만, 전달한 값이 다르기 때문에 각 객체가 가진 데이터도 다르다.
각 객체의 값은 아래처럼 이해할 수 있다.
st1은 이름이"둘리"이고 점수는96,98,92,98이다.st2는 이름이"또치"이고 점수는92,98,96,98이다.st3는 이름이"도우너"이고 점수는76,96,94,90이다.같은
class로 만든 객체라도, 생성할 때 전달한 값에 따라 서로 다른 데이터를 가진다.
toString으로 객체를 문자열로 만들기
// exam13.html // 객체를 문자열로 표현할 때 사용할 메서드이다. toString() { // 현재 객체가 가진 이름과 점수를 하나의 문자열로 만들어 반환한다. return ( this.이름 + " : " + this.국어 + ", " + this.수학 + ", " + this.영어 + ", " + this.과학 ); }
toString()은 객체를 문자열로 표현할 때 사용할 수 있는 메서드이다.
메서드는 객체 안에 들어 있는 함수라고 이해하면 된다.
이 예제의toString()은 현재 객체가 가진 이름과 점수를 하나의 문자열로 만든다.
this.이름,this.국어,this.수학,this.영어,this.과학을 차례대로 이어 붙여 문자열을 만든다.
예를 들어st1에 대해toString()이 사용되면 아래와 같은 문자열이 만들어진다.// 출력결과 // 둘리 : 96, 98, 92, 98여기서
this는st1을 가리킨다.
그래서this.이름은"둘리"이고,this.국어는96이다.
toString()은 객체를 사람이 읽을 수 있는 문자열 형태로 표현할 때 사용하는 메서드이다.
write로 객체 출력하기
// exam13.html // st1 객체를 h2 태그로 화면에 출력한다. write(st1, "h2"); // st2 객체를 h2 태그로 화면에 출력한다. write(st2, "h2"); // st3 객체를 h2 태그로 화면에 출력한다. write(st3, "h2");
write()는util.js에서 불러온 보조 함수이다.
이 함수는 전달받은 내용을 지정한HTML태그로 화면에 출력하는 역할을 한다.
여기서는write(st1, "h2")처럼 객체와"h2"를 전달한다.
그러면st1객체를h2태그 형태로 화면에 출력한다.
그런데st1은 숫자나 문자열이 아니라 객체이다.
객체를 화면에 문자열처럼 출력해야 하므로, 객체의toString()결과가 사용된다.
그래서st1은"둘리 : 96, 98, 92, 98"형태로 출력된다.
st2,st3도 같은 방식으로 출력된다.
각 객체가 가진 값이 다르기 때문에 출력되는 문자열도 다르다.
객체를 문자열처럼 출력할 때toString()이 정의되어 있으면 그 결과 문자열이 사용된다.
전체 출력 결과
이 예제를 실행하면 화면에는 학생 세 명의 정보가 출력된다.
각 정보는h2태그로 출력된다.// 출력결과 // 둘리 : 96, 98, 92, 98 // 또치 : 92, 98, 96, 98 // 도우너 : 76, 96, 94, 90첫 번째 줄은
st1객체의 출력 결과이다.
st1은"둘리"와 점수96,98,92,98을 가진 객체이다.
두 번째 줄은st2객체의 출력 결과이다.
st2는"또치"와 점수92,98,96,98을 가진 객체이다.
세 번째 줄은st3객체의 출력 결과이다.
st3는"도우너"와 점수76,96,94,90을 가진 객체이다.
같은Studentclass로 만든 객체라도, 생성할 때 전달한 값이 다르면 출력 결과도 달라진다.
class와 객체 생성 흐름 정리하기
이 예제의 전체 흐름은 아래처럼 볼 수 있다.
Studentclass를 만든다.constructor에서 학생 정보를 받을 준비를 한다.new Student(...)로 실제 학생 객체를 만든다.constructor가 자동으로 실행된다.- 전달된 값들이
this.이름,this.국어,this.수학,this.영어,this.과학에 저장된다.toString()은 객체 정보를 문자열로 만든다.write()는 객체를 화면에 출력한다.이 흐름에서 가장 중요한 연결은
new와constructor이다.
new Student(...)를 실행하면Student객체가 만들어지고, 그 순간constructor가 실행되어 값을 저장한다.
class는 객체의 구조를 정하고,new는 그 구조를 바탕으로 실제 객체를 만들며,constructor는 객체가 만들어질 때 초기값을 저장한다.
핵심 정리
이 예제의 핵심은
class를 사용해서 같은 구조의 객체를 여러 개 만드는 방법이다.
Studentclass는 학생 객체의 구조를 정하고,new Student(...)는 실제 학생 객체를 만든다.
이 흐름은 아래처럼 정리할 수 있다.
class는 객체를 만들기 위한 틀이다.Student는 학생 객체를 만들기 위한class이다.constructor는 객체가 생성될 때 자동으로 실행된다.constructor는 전달받은 값을 현재 객체 안에 저장한다.this는 현재 만들어지고 있는 객체를 가리킨다.this.이름 = name은 현재 객체의 이름 프로퍼티에name값을 저장한다.new Student(...)는Studentclass로 새 객체를 만든다.st1,st2,st3는 서로 다른 학생 객체이다.toString()은 객체 정보를 문자열로 만들어 반환한다.write()는 객체를 화면에 출력한다.따라서 이 예제가 알려 주는 핵심은 이것이다.
class는 객체를 만들기 위한 틀이고,new로 객체를 만들면constructor가 실행되어 전달받은 값이this를 통해 현재 객체 안에 저장된다.
이 예제는
class안에서 데이터를 저장하는 방식이 어떻게 달라지는지 보여 준다.
처음에는this.age처럼 밖에서도 바로 접근할 수 있는 공개 프로퍼티를 사용한다.
그다음#age처럼 클래스 밖에서 직접 접근할 수 없는 비공개 프로퍼티로 바꾼다.
비공개 프로퍼티는 값을 함부로 건드리지 못하게 막을 때 사용한다.
하지만 값을 완전히 못 쓰게 막으면 실제 코드에서 불편하다.
그래서 값을 읽는 통로인getter와 값을 바꾸는 통로인setter를 함께 사용한다.
이 예제의 핵심은 객체 안의 값을 그냥 공개할지, 비공개로 숨긴 뒤 정해진 통로로만 읽고 바꿀지 이해하는 것이다.
프로퍼티란 무엇인가
프로퍼티는 객체 안에 저장되는 값이다.
객체가 가진 데이터라고 이해하면 된다.
앞에서Student객체는이름,국어,수학,영어,과학같은 값을 가지고 있었다.
이번 예제의Person객체는name과age를 가진다.
예를 들어 아래 코드는 객체 안에name과age를 저장하는 흐름이다.// exam14.html // 현재 객체에 name 프로퍼티를 만든다. this.name = name; // 현재 객체에 age 프로퍼티를 만든다. this.age = age;
this.name은 현재 만들어지는 객체의name프로퍼티이다.
this.age는 현재 만들어지는 객체의age프로퍼티이다.
객체가 만들어진 뒤에는p1.name,p1.age처럼 객체 밖에서 값을 바로 읽을 수 있다.
이렇게 밖에서 바로 접근할 수 있는 프로퍼티를 공개 프로퍼티라고 이해하면 된다.
공개 프로퍼티는 객체 밖에서객체명.프로퍼티명형태로 바로 읽거나 바꿀 수 있는 값이다.
비공개 프로퍼티란 무엇인가
비공개 프로퍼티는 클래스 밖에서 직접 접근할 수 없게 숨겨 둔 값이다.
JavaScript에서는 프로퍼티 이름 앞에#을 붙이면 비공개 프로퍼티가 된다.
예를 들어#age는 비공개 프로퍼티이다.
이 값은class안에서는 사용할 수 있지만, 클래스 밖에서p1.#age처럼 직접 접근할 수 없다.
비공개 프로퍼티가 필요한 이유는 객체 안의 중요한 값을 함부로 바꾸지 못하게 하기 위해서이다.
나이처럼 객체 내부에서 관리해야 하는 값은 아무 곳에서나 마음대로 바뀌면 코드 흐름이 불안정해질 수 있다.
정리하면 아래처럼 볼 수 있다.
this.age는 공개 프로퍼티이다.#age는 비공개 프로퍼티이다.- 공개 프로퍼티는 객체 밖에서 바로 접근할 수 있다.
- 비공개 프로퍼티는 클래스 밖에서 직접 접근할 수 없다.
- 비공개 값은
class안에서 정해 둔 메서드나 접근 통로를 통해 다루는 것이 안전하다.비공개 프로퍼티는 객체 내부의 값을 외부에서 직접 건드리지 못하게 보호하기 위한 문법이다.
getter와 setter란 무엇인가
getter는 비공개 값을 읽을 수 있게 해 주는 통로이다.
값을 직접 꺼내는 것이 아니라, 정해진 메서드를 통해 값을 반환하게 만든다.
setter는 비공개 값을 바꿀 수 있게 해 주는 통로이다.
외부에서 값을 직접 바꾸는 것이 아니라, 정해진 메서드를 통해 값을 변경하게 만든다.
getter와setter는 메서드처럼 작성하지만 사용할 때는 프로퍼티처럼 접근한다.
예를 들어get age()가 있으면p1.age처럼 값을 읽을 수 있다.
set age(age)가 있으면p1.age = 20처럼 값을 바꿀 수 있다.
여기서 중요한 점은get age()와set age(age)의 이름이 둘 다age라는 것이다.
그래서p1.age를 읽을 때는getter가 실행되고,p1.age = 값처럼 저장할 때는setter가 실행된다.
getter와setter는 비공개 값을 직접 공개하지 않고, 같은 이름의 읽기 통로와 쓰기 통로를 제공한다.
예제 전체 코드 1
// exam14.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>Insert title here</title> </head> <body> <h1>자바스크립트의 객체 생성과 사용(클래스)</h1> <hr /> <!-- 화면 출력용 보조 함수 파일을 불러온다. --> <script src="../util.js"></script> <script> // Person 클래스는 사람 객체를 만들기 위한 틀이다. class Person { // constructor는 객체가 만들어질 때 자동으로 실행된다. constructor(name, age) { // name은 공개 프로퍼티로 저장한다. this.name = name; // age도 공개 프로퍼티로 저장한다. this.age = age; } // 인사 문장을 화면에 출력하는 메서드이다. hello(color) { // 현재 객체의 name과 age를 사용해 문장을 만든다. writeColor(`${this.name} 안녕! 내 나이는 ${this.age}세야~~~`, "h3", color); } } // Person 클래스로 객체 세 개를 만든다. let p1 = new Person("둘리", 10); let p2 = new Person("또치", 12); let p3 = new Person("도우너", 11); // 각 객체의 hello 메서드를 실행한다. p1.hello("green"); p2.hello("orange"); p3.hello("red"); // 화면에 구분선을 출력한다. hr(); // 공개 프로퍼티 name과 age를 객체 밖에서 바로 읽는다. writeNewLine(p1.name); writeNewLine(p1.age); writeNewLine(p2.name); writeNewLine(p2.age); writeNewLine(p3.name); writeNewLine(p3.age); </script> </body> </html>이 코드는
name과age를 모두 공개 프로퍼티로 저장한다.
그래서 객체 밖에서p1.name,p1.age처럼 바로 값을 읽을 수 있다.
hello()메서드 안에서도this.name,this.age를 사용한다.
이때this는 현재hello()를 호출한 객체를 가리킨다.
따라서p1.hello("green")에서는this가p1을 가리킨다.
exam14.html은 공개 프로퍼티를 사용하면 객체 밖에서 값을 바로 읽을 수 있다는 것을 보여 준다.
예제 전체 코드 2
// exam14_1.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>Insert title here</title> </head> <body> <h1>자바스크립트의 객체 생성과 사용(클래스)</h1> <hr /> <!-- 화면 출력용 보조 함수 파일을 불러온다. --> <script src="../util.js"></script> <script> // Person 클래스는 사람 객체를 만들기 위한 틀이다. class Person { // #age는 클래스 밖에서 직접 접근할 수 없는 비공개 프로퍼티이다. #age // constructor는 객체가 만들어질 때 자동으로 실행된다. constructor(name, age) { // name은 공개 프로퍼티로 저장한다. this.name = name; // age는 비공개 프로퍼티 #age에 저장한다. this.#age = age; } // 인사 문장을 화면에 출력하는 메서드이다. hello(color) { // 클래스 안에서는 #age에 접근할 수 있다. writeColor(`${this.name} 안녕! 내 나이는 ${this.#age}세야~~~`, "h3", color); } } // Person 클래스로 객체 세 개를 만든다. let p1 = new Person("둘리", 10); let p2 = new Person("또치", 12); let p3 = new Person("도우너", 11); // 각 객체의 hello 메서드를 실행한다. p1.hello("green"); p2.hello("orange"); p3.hello("red"); // 화면에 구분선을 출력한다. hr(); // name은 공개 프로퍼티이므로 객체 밖에서 바로 읽을 수 있다. writeNewLine(p1.name); // age라는 공개 프로퍼티가 없으므로 undefined가 나온다. writeNewLine(p1.age); writeNewLine(p2.name); writeNewLine(p2.age); writeNewLine(p3.name); writeNewLine(p3.age); // #age는 비공개 프로퍼티이므로 클래스 밖에서 직접 접근하면 문법 오류가 난다. // writeNewLine(p1.#age); </script> </body> </html>이 코드는
age를 공개 프로퍼티가 아니라#age라는 비공개 프로퍼티로 저장한다.
그래서hello()메서드 안에서는this.#age로 나이를 사용할 수 있다.
하지만 객체 밖에서는p1.#age처럼 직접 접근할 수 없다.
또 이 코드에는p1.age가 있다.
하지만constructor에서this.age = age를 만든 적이 없다.
나이는this.#age = age에 저장되었다.
따라서p1.age는 존재하지 않는 공개 프로퍼티를 읽으려는 코드가 되고, 결과는undefined가 된다.
#age로 저장하면 객체 안에는 나이가 있지만, 객체 밖에서p1.age로 바로 읽을 수는 없다.
예제 전체 코드 3
// exam14_2.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>Insert title here</title> </head> <body> <h1>자바스크립트의 객체 생성과 사용(클래스)</h1> <hr /> <!-- 화면 출력용 보조 함수 파일을 불러온다. --> <script src="../util.js"></script> <script> // Person 클래스는 사람 객체를 만들기 위한 틀이다. class Person { // #age는 클래스 밖에서 직접 접근할 수 없는 비공개 프로퍼티이다. #age // constructor는 객체가 만들어질 때 자동으로 실행된다. constructor(name, age) { // name은 공개 프로퍼티로 저장한다. this.name = name; // age는 비공개 프로퍼티 #age에 저장한다. this.#age = age; } // 인사 문장을 화면에 출력하는 메서드이다. hello(color) { // 클래스 안에서는 #age에 접근할 수 있다. writeColor(`${this.name} 안녕! 내 나이는 ${this.#age}세야~~~`, "h3", color); } // 비공개 프로퍼티 #age 값을 읽어 주는 getter이다. get age() { // #age 값을 외부에서 p1.age처럼 읽을 수 있게 반환한다. return this.#age; } } // Person 클래스로 객체 세 개를 만든다. let p1 = new Person("둘리", 10); let p2 = new Person("또치", 12); let p3 = new Person("도우너", 11); // 각 객체의 hello 메서드를 실행한다. p1.hello("green"); p2.hello("orange"); p3.hello("red"); // 화면에 구분선을 출력한다. hr(); // name은 공개 프로퍼티이고, age는 getter를 통해 읽는다. writeNewLine(p1.name); writeNewLine(p1.age); writeNewLine(p2.name); writeNewLine(p2.age); writeNewLine(p3.name); writeNewLine(p3.age); // #age는 비공개 프로퍼티이므로 클래스 밖에서 직접 접근하면 문법 오류가 난다. // writeNewLine(p1.#age); </script> </body> </html>이 코드는
#age를 비공개로 유지하면서도get age()를 추가한다.
그래서 객체 밖에서p1.age처럼 값을 읽을 수 있다.
중요한 점은p1.age가#age에 직접 접근하는 것이 아니라는 점이다.
p1.age를 읽으면 내부적으로get age()가 실행되고, 그 안에서this.#age를 반환한다.
getter를 사용하면 비공개 프로퍼티를 직접 공개하지 않고도 값을 읽을 수 있는 통로를 만들 수 있다.
예제 전체 코드 4
// exam14_3.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>Insert title here</title> </head> <body> <h1>자바스크립트의 객체 생성과 사용(클래스)</h1> <hr /> <!-- 화면 출력용 보조 함수 파일을 불러온다. --> <script src="../util.js"></script> <script> // Person 클래스는 사람 객체를 만들기 위한 틀이다. class Person { // #age는 클래스 밖에서 직접 접근할 수 없는 비공개 프로퍼티이다. #age // constructor는 객체가 만들어질 때 자동으로 실행된다. constructor(name, age) { // name은 공개 프로퍼티로 저장한다. this.name = name; // age는 비공개 프로퍼티 #age에 저장한다. this.#age = age; } // 인사 문장을 화면에 출력하는 메서드이다. hello(color) { // 클래스 안에서는 #age에 접근할 수 있다. writeColor(`${this.name} 안녕! 내 나이는 ${this.#age}세야~~~`, "h3", color); } // 비공개 프로퍼티 #age 값을 읽어 주는 getter이다. get age() { // #age 값을 외부에서 p1.age처럼 읽을 수 있게 반환한다. return this.#age; } // 비공개 프로퍼티 #age 값을 바꿔 주는 setter이다. set age(age) { // 외부에서 전달된 값을 #age에 저장한다. this.#age = age; } } // Person 클래스로 객체 세 개를 만든다. let p1 = new Person("둘리", 10); let p2 = new Person("또치", 12); let p3 = new Person("도우너", 11); // 처음 나이로 인사 문장을 출력한다. p1.hello("green"); p2.hello("orange"); p3.hello("red"); // 화면에 구분선을 출력한다. hr(); // age getter로 기존 값을 읽고, setter로 증가된 값을 다시 저장한다. p1.age += 10; p2.age += 10; p3.age += 10; // 나이가 변경된 뒤 다시 인사 문장을 출력한다. p1.hello("green"); p2.hello("orange"); p3.hello("red"); // 화면에 구분선을 출력한다. hr(); // getter를 통해 변경된 나이 값을 읽어 출력한다. writeNewLine(p1.age); writeNewLine(p2.age); writeNewLine(p3.age); </script> </body> </html>이 코드는
getter와setter를 모두 사용한다.
get age()는#age값을 읽는 통로이고,set age(age)는#age값을 바꾸는 통로이다.
p1.age += 10은 단순해 보이지만 내부적으로는 읽기와 쓰기가 모두 일어난다.
먼저p1.age를 읽으면서getter가 실행된다.
그다음 읽어 온 값에10을 더한다.
마지막으로 더한 값을 다시p1.age에 저장하면서setter가 실행된다.
p1.age += 10은getter로 값을 읽고, 계산한 뒤,setter로 다시 저장하는 흐름이다.
이 예제에서 확인할 핵심
this.name은 공개 프로퍼티이다.this.age도 공개 프로퍼티이다.#age는 비공개 프로퍼티이다.- 비공개 프로퍼티는 클래스 밖에서 직접 접근할 수 없다.
p1.age는 공개 프로퍼티가 있으면 바로 값을 읽는다.#age만 있고get age()가 없으면p1.age는undefined가 된다.get age()는#age값을 읽는 통로이다.set age(age)는#age값을 바꾸는 통로이다.getter와setter는 메서드처럼 만들지만 사용할 때는 프로퍼티처럼 사용한다.p1.age += 10은 값을 읽고 다시 저장하는 과정이다.이 예제는 공개 프로퍼티와 비공개 프로퍼티의 차이, 그리고
getter와setter가 필요한 이유를 단계별로 보여 준다.
공개 프로퍼티로 age 저장하기
// exam14.html class Person { constructor(name, age) { // name은 공개 프로퍼티이다. this.name = name; // age도 공개 프로퍼티이다. this.age = age; } hello(color) { // 공개 프로퍼티 name과 age를 사용한다. writeColor(`${this.name} 안녕! 내 나이는 ${this.age}세야~~~`, "h3", color); } }이 코드는
name과age를 모두 공개 프로퍼티로 저장한다.
this.name = name은 현재 객체에name프로퍼티를 만든다.
this.age = age는 현재 객체에age프로퍼티를 만든다.
공개 프로퍼티는 객체 밖에서 바로 읽을 수 있다.
그래서 아래 코드가 가능하다.// exam14.html writeNewLine(p1.name); writeNewLine(p1.age);
p1.name은"둘리"를 읽는다.
p1.age는10을 읽는다.
화면 출력 결과는 아래처럼 볼 수 있다.// 출력결과 // 둘리 안녕! 내 나이는 10세야~~~ // 또치 안녕! 내 나이는 12세야~~~ // 도우너 안녕! 내 나이는 11세야~~~ // 둘리 // 10 // 또치 // 12 // 도우너 // 11공개 프로퍼티는 객체 밖에서 바로 읽을 수 있기 때문에 사용하기 쉽지만, 값이 외부에 그대로 노출된다.
비공개 프로퍼티로 age 숨기기
// exam14_1.html class Person { // #age는 비공개 프로퍼티이다. #age constructor(name, age) { // name은 공개 프로퍼티이다. this.name = name; // age는 #age에 저장한다. this.#age = age; } hello(color) { // 클래스 안에서는 #age를 사용할 수 있다. writeColor(`${this.name} 안녕! 내 나이는 ${this.#age}세야~~~`, "h3", color); } }이 코드는
age를this.age가 아니라this.#age에 저장한다.
#age는 비공개 프로퍼티이므로 클래스 밖에서 직접 접근할 수 없다.
하지만 클래스 안에서는 사용할 수 있다.
그래서hello()메서드 안에서는this.#age를 사용해서 나이를 출력할 수 있다.
객체 밖에서p1.age를 읽으면 어떻게 될까.
p1객체에는 공개 프로퍼티age가 없다.
나이는 비공개 프로퍼티#age에 저장되어 있다.
그래서p1.age는 존재하지 않는 공개 프로퍼티를 읽는 코드가 되고, 결과는undefined가 된다.
화면 출력 결과는 아래처럼 볼 수 있다.// 출력결과 // 둘리 안녕! 내 나이는 10세야~~~ // 또치 안녕! 내 나이는 12세야~~~ // 도우너 안녕! 내 나이는 11세야~~~ // 둘리 // undefined // 또치 // undefined // 도우너 // undefined여기서
undefined는 값이 정의되어 있지 않다는 뜻이다.
즉,p1.age라는 공개 프로퍼티가 없다는 의미이다.
#age는 객체 안에 존재하지만, 클래스 밖에서p1.age로 바로 읽을 수 있는 공개 프로퍼티는 아니다.
p1.#age를 직접 읽으면 안 되는 이유
exam14_1.html에는 아래 코드가 주석 처리되어 있다.// exam14_1.html // #age는 비공개 프로퍼티이므로 클래스 밖에서 직접 접근하면 문법 오류가 난다. // writeNewLine(p1.#age);이 코드는 실행하면 안 된다.
#age는 비공개 프로퍼티이므로 클래스 바깥에서 직접 접근할 수 없기 때문이다.
p1.age처럼 쓰면 존재하지 않는 공개 프로퍼티를 읽는 것이므로undefined가 나올 수 있다.
하지만p1.#age처럼 쓰면 아예 비공개 프로퍼티에 클래스 밖에서 접근하는 것이므로 문법 오류가 난다.
둘은 반드시 구분해야 한다.
p1.age는 공개 프로퍼티age를 읽으려는 코드이다.- 공개 프로퍼티
age가 없으면undefined가 나온다.p1.#age는 비공개 프로퍼티#age를 직접 읽으려는 코드이다.- 비공개 프로퍼티는 클래스 밖에서 직접 접근할 수 없으므로 오류가 난다.
p1.age가undefined인 것과p1.#age가 문법 오류인 것은 서로 다른 문제이다.
getter로 비공개 age 읽기
// exam14_2.html class Person { #age constructor(name, age) { this.name = name; this.#age = age; } hello(color) { writeColor(`${this.name} 안녕! 내 나이는 ${this.#age}세야~~~`, "h3", color); } // #age 값을 읽어 주는 getter이다. get age() { return this.#age; } }이 코드에서는
#age를 비공개로 유지하면서get age()를 추가했다.
get age()는 비공개 프로퍼티#age의 값을 반환한다.
이제 객체 밖에서p1.age를 읽으면undefined가 나오지 않는다.
p1.age를 읽는 순간get age()가 실행되고, 그 결과로this.#age값이 반환된다.
즉, 아래 코드는#age에 직접 접근하는 것이 아니다.// exam14_2.html writeNewLine(p1.age);겉으로는
p1.age처럼 보이지만, 실제로는get age()라는 읽기 통로를 사용하는 것이다.
화면 출력 결과는 아래처럼 볼 수 있다.// 출력결과 // 둘리 안녕! 내 나이는 10세야~~~ // 또치 안녕! 내 나이는 12세야~~~ // 도우너 안녕! 내 나이는 11세야~~~ // 둘리 // 10 // 또치 // 12 // 도우너 // 11
getter는 비공개 프로퍼티를 직접 공개하지 않고, 값을 읽을 수 있는 통로를 제공한다.
setter로 비공개 age 변경하기
// exam14_3.html class Person { #age constructor(name, age) { this.name = name; this.#age = age; } hello(color) { writeColor(`${this.name} 안녕! 내 나이는 ${this.#age}세야~~~`, "h3", color); } get age() { return this.#age; } set age(age) { this.#age = age; } }이 코드에서는
get age()와 함께set age(age)를 추가했다.
set age(age)는 외부에서 전달된 값을 비공개 프로퍼티#age에 저장한다.
여기서get age()와set age(age)는 같은 이름인age를 사용한다.
그래서p1.age처럼 읽을 때는get age()가 실행되고,p1.age = 20처럼 값을 넣을 때는set age(age)가 실행된다.
이제 객체 밖에서p1.age = 20처럼 값을 바꿀 수 있다.
하지만 실제로는#age에 직접 접근하는 것이 아니다.
p1.age = 20을 실행하면 내부적으로set age(age)가 실행되고, 그 안에서this.#age = age가 수행된다.
setter는 비공개 프로퍼티를 직접 수정하지 않고, 정해진 통로를 통해 값을 변경하게 해 준다.
p1.age += 10의 내부 흐름
// exam14_3.html // age getter로 기존 값을 읽고, setter로 증가된 값을 다시 저장한다. p1.age += 10; p2.age += 10; p3.age += 10;
p1.age += 10은 짧은 코드이지만 내부적으로는 두 단계가 일어난다.
먼저p1.age를 읽어야 한다.
이때get age()가 실행되어 현재#age값인10을 반환한다.
그다음 반환된 값에10을 더한다.
그래서10 + 10이 되어20이 된다.
마지막으로 계산된20을 다시p1.age에 저장한다.
이때set age(age)가 실행되고,this.#age = age를 통해#age가20으로 바뀐다.
흐름은 아래처럼 볼 수 있다.
p1.age를 읽는다.get age()가 실행된다.#age값10이 반환된다.10 + 10을 계산한다.- 계산 결과
20을p1.age에 저장한다.set age(age)가 실행된다.#age값이20으로 변경된다.복합 대입 연산자인
+=은 값을 읽는 과정과 다시 저장하는 과정을 모두 포함하므로, 이 예제에서는getter와setter가 모두 실행된다.
getter와 setter 사용 후 화면 출력 결과
exam14_3.html에서는 처음 나이를 출력한 뒤, 나이를10씩 증가시키고 다시 출력한다.
그리고 마지막에 변경된 나이만 다시 출력한다.
출력 흐름은 아래처럼 볼 수 있다.// 출력결과 // 둘리 안녕! 내 나이는 10세야~~~ // 또치 안녕! 내 나이는 12세야~~~ // 도우너 안녕! 내 나이는 11세야~~~ // 둘리 안녕! 내 나이는 20세야~~~ // 또치 안녕! 내 나이는 22세야~~~ // 도우너 안녕! 내 나이는 21세야~~~ // 20 // 22 // 21처음에는 생성자에서 받은 값 그대로 출력된다.
그래서 둘리는10, 또치는12, 도우너는11이다.
그다음p1.age += 10,p2.age += 10,p3.age += 10이 실행된다.
각 객체의#age값이10씩 증가한다.
그래서 둘리는20, 또치는22, 도우너는21이 된다.
마지막의writeNewLine(p1.age),writeNewLine(p2.age),writeNewLine(p3.age)는getter를 통해 변경된 나이 값을 읽어 출력한다.
getter와setter를 함께 사용하면 비공개 값을 보호하면서도 필요한 방식으로 읽고 수정할 수 있다.
공개 프로퍼티와 비공개 프로퍼티 비교하기
공개 프로퍼티와 비공개 프로퍼티는 값을 저장한다는 점에서는 비슷하다.
하지만 외부에서 접근할 수 있는지가 다르다.
this.age처럼 만든 공개 프로퍼티는 객체 밖에서p1.age로 바로 접근할 수 있다.
그래서 사용하기 쉽지만, 외부 코드가 값을 직접 바꿀 수도 있다.
반면#age는 클래스 밖에서 직접 접근할 수 없다.
그래서 값을 보호할 수 있다.
대신 값을 읽거나 바꾸려면getter와setter같은 통로를 만들어야 한다.
정리하면 아래처럼 볼 수 있다.
- 공개 프로퍼티는 객체 밖에서 바로 접근할 수 있다.
- 비공개 프로퍼티는 클래스 밖에서 직접 접근할 수 없다.
- 공개 프로퍼티는 사용하기 쉽지만 외부에 값이 노출된다.
- 비공개 프로퍼티는 값을 보호할 수 있지만 읽기와 변경 통로가 필요하다.
getter는 비공개 값을 읽는 통로이다.setter는 비공개 값을 바꾸는 통로이다.중요한 값은 비공개 프로퍼티로 숨기고, 필요한 경우
getter와setter로 통제된 접근을 제공할 수 있다.
핵심 정리
이 예제의 핵심은 클래스 안의 값을 공개할지, 비공개로 숨길지, 그리고 숨긴 값을 어떻게 읽고 바꿀지 이해하는 것이다.
공개 프로퍼티는 바로 접근할 수 있고, 비공개 프로퍼티는 클래스 밖에서 직접 접근할 수 없다.
이 흐름은 아래처럼 정리할 수 있다.
this.name = name은 공개 프로퍼티name을 만든다.this.age = age는 공개 프로퍼티age를 만든다.- 공개 프로퍼티는
p1.age처럼 객체 밖에서 바로 읽을 수 있다.#age는 비공개 프로퍼티이다.this.#age = age는 나이를 비공개 프로퍼티에 저장한다.- 클래스 안에서는
this.#age를 사용할 수 있다.- 클래스 밖에서
p1.#age처럼 직접 접근할 수 없다.#age만 있고get age()가 없으면p1.age는undefined가 된다.get age()는#age값을 읽을 수 있게 해 주는getter이다.set age(age)는#age값을 바꿀 수 있게 해 주는setter이다.get age()와set age(age)는 같은 이름인age를 통해 읽기와 쓰기 동작을 연결한다.p1.age += 10은getter로 값을 읽고, 계산한 뒤,setter로 다시 저장하는 흐름이다.따라서 이 예제가 알려 주는 핵심은 이것이다.
비공개 프로퍼티는 값을 외부에서 직접 건드리지 못하게 보호하고,getter와setter는 그 값을 읽고 바꾸는 정해진 통로를 제공한다.
이 예제는
class상속을 사용해서 공통 기능은 부모 클래스에 두고, 세부 도형 기능은 자식 클래스에서 다르게 구현하는 흐름을 보여 준다.
상속은 이미 만들어진 클래스의 기능을 다른 클래스가 물려받아 사용하는 문법이다.
여기서는Shape클래스가 공통 부모 역할을 한다.
Rectangle,Triangle,Circle클래스는Shape를 상속받는 자식 클래스이다.
각 도형은 색상을 가지고 도형을 그리는 공통 기능을 사용하지만, 넓이를 구하는 방식은 서로 다르다.
이 예제의 핵심은 공통 기능은 부모 클래스에 두고, 자식 클래스에서 필요한 기능만 추가하거나 다시 정의하는 흐름을 이해하는 것이다.
상속이란 무엇인가
상속은 부모 클래스의 기능을 자식 클래스가 물려받아 사용하는 문법이다.
부모 클래스는 공통으로 필요한 값과 기능을 가지고, 자식 클래스는 그 공통 기능을 이어받아 자기 상황에 맞게 사용한다.
예를 들어 사각형, 삼각형, 원은 모두 도형이다.
이 도형들은 모두 색상을 가질 수 있고, 화면에 그려질 수 있다.
이 공통점을Shape클래스에 넣어 두면, 사각형과 삼각형과 원이 같은 코드를 반복해서 작성하지 않아도 된다.
상속 관계는 아래처럼 이해하면 된다.
Shape는 부모 클래스이다.Rectangle은Shape를 상속받는 자식 클래스이다.Triangle은Shape를 상속받는 자식 클래스이다.Circle은Shape를 상속받는 자식 클래스이다.- 자식 클래스는 부모 클래스의 프로퍼티와 메서드를 사용할 수 있다.
상속은 여러 클래스가 공통으로 가져야 하는 코드를 부모 클래스에 모아 두고, 자식 클래스가 그것을 이어받아 사용하게 하는 구조이다.
extends와 super란 무엇인가
extends는 어떤 클래스를 상속받을지 정하는 문법이다.
예를 들어class Rectangle extends Shape라고 쓰면Rectangle클래스가Shape클래스를 상속받는다는 뜻이다.
super는 부모 클래스를 가리킬 때 사용하는 문법이다.
자식 클래스에서 부모 생성자를 호출하거나, 부모 클래스의 메서드를 실행할 때 사용한다.
이 예제에서는super(color),super.draw(),super.getArea()가 나온다.
super(color)는 부모 클래스의constructor를 호출해서 부모 쪽 초기화 작업을 실행한다.
super.draw()는 부모 클래스의draw()메서드를 먼저 실행한다.
super.getArea()는 부모 클래스의getArea()메서드를 실행한다.
정리하면 아래처럼 볼 수 있다.
extends는 부모 클래스를 상속받겠다는 뜻이다.super()는 부모 클래스의 생성자를 호출한다.super.draw()는 부모 클래스의draw()메서드를 호출한다.super.getArea()는 부모 클래스의getArea()메서드를 호출한다.- 자식 클래스에서
this를 사용하기 전에super()를 먼저 호출해야 한다.자식 클래스가 부모 클래스의 초기화 작업을 사용하려면
constructor안에서super()를 먼저 호출해야 한다.
메서드 재정의란 무엇인가
메서드 재정의는 부모 클래스에 있는 메서드를 자식 클래스에서 다시 만드는 것이다.
영어로는overriding이라고 한다.
부모 클래스에 이미draw()가 있어도, 자식 클래스에서draw()를 다시 만들 수 있다.
그러면 자식 객체가draw()를 호출할 때 부모의draw()가 아니라 자식 클래스에 새로 정의된draw()가 실행된다.
이 예제에서는Triangle,Rectangle,Circle이 각자draw()와getArea()를 자기 방식에 맞게 작성한다.
도형마다 넓이 계산 공식이 다르기 때문이다.
예를 들어 사각형의 넓이는width * height이다.
삼각형의 넓이는(width * height) / 2이다.
원의 넓이는radius * radius * Math.PI이다.
메서드 재정의는 부모에게 물려받은 동작을 자식 클래스 상황에 맞게 다시 작성하는 것이다.
예제 전체 코드 1
// exam15.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>Insert title here</title> </head> <body> <h1>자바스크립트의 객체 생성과 사용(클래스)</h1> <hr /> <!-- 화면 출력용 보조 함수 파일을 불러온다. --> <script src="../util.js"></script> <script> // Shape 클래스는 도형의 공통 정보를 가진 부모 클래스이다. class Shape { // width, height, color 값을 받아 현재 객체에 저장한다. constructor(width, height, color) { this.width = width; this.height = height; this.color = color; } // 도형의 색상 정보를 화면에 출력한다. draw() { write(`${this.color} 색상으로 도형을 그린다.`, "h3"); } // 기본 넓이는 가로 * 세로로 계산한다. getArea() { return this.width * this.height; } } // Rectangle은 Shape를 상속받는다. // 별도 constructor가 없으므로 Shape의 constructor 흐름을 그대로 사용한다. class Rectangle extends Shape {} // Triangle은 Shape를 상속받는다. class Triangle extends Shape { // 삼각형 넓이는 부모의 넓이 계산 결과를 2로 나눈다. getArea() { return super.getArea() / 2; } // 부모의 draw를 먼저 실행한 뒤 삼각형 문장을 추가로 출력한다. draw() { super.draw(); write(`삼각형을 그린다.`, "h3"); } } // Rectangle 객체를 만든다. const rectangle = new Rectangle(20, 20, "파랑"); // 사각형 그리기 문장을 출력한다. rectangle.draw(); // 사각형 넓이를 출력한다. writeColor(rectangle.getArea(), "h2", "blue"); // 화면에 구분선을 출력한다. hr(); // Triangle 객체를 만든다. const triangle = new Triangle(20, 20, "빨강"); // 삼각형 그리기 문장을 출력한다. triangle.draw(); // 삼각형 넓이를 출력한다. writeColor(triangle.getArea(), "h2", "red"); </script> </body> </html>
exam15.html은 가장 기본적인 상속 흐름을 보여 준다.
Shape클래스가width,height,color를 저장하고,draw()와getArea()메서드를 가진다.
Rectangle은Shape를 상속받기만 하고 별도의 코드를 작성하지 않는다.
그래서Shape의constructor,draw(),getArea()를 그대로 사용한다.
반면Triangle은getArea()와draw()를 다시 정의한다.
삼각형은 넓이를 구할 때 사각형처럼width * height만 사용하면 안 된다.
그래서 부모의getArea()결과를 가져온 뒤2로 나누어 삼각형 넓이를 만든다.
Rectangle은 부모 기능을 그대로 사용하고,Triangle은 부모 기능을 일부 다시 정의해서 자기 도형에 맞게 바꾼다.
예제 전체 코드 2
// exam15_1.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>Insert title here</title> </head> <body> <h1>자바스크립트의 객체 생성과 사용(클래스)</h1> <hr /> <!-- 화면 출력용 보조 함수 파일을 불러온다. --> <script src="../util.js"></script> <script> // Shape 클래스는 모든 도형이 공통으로 가지는 색상과 draw 기능을 가진다. class Shape { // color 값을 받아 현재 객체에 저장한다. constructor(color) { this.color = color; } // 도형의 공통 그리기 문장을 출력한다. draw() { write(`${this.color} 색상으로 도형을 그린다.`, "h3"); } } // Rectangle은 Shape를 상속받는다. class Rectangle extends Shape { // 사각형은 width, height, color가 필요하다. constructor(width, height, color) { // 부모 constructor에 color를 전달한다. super(color); // 사각형만의 가로와 세로를 저장한다. this.width = width; this.height = height; } // 사각형 넓이를 계산한다. getArea() { return this.width * this.height; } // 부모 draw를 실행한 뒤 사각형 문장을 추가한다. draw() { super.draw(); write(`사각형을 그린다.`, "h3"); } } // Triangle은 Shape를 상속받는다. class Triangle extends Shape { // 삼각형은 width, height, color가 필요하다. constructor(width, height, color) { // 부모 constructor에 color를 전달한다. super(color); // 삼각형만의 가로와 세로를 저장한다. this.width = width; this.height = height; } // 삼각형 넓이를 계산한다. getArea() { return (this.width * this.height) / 2; } // 부모 draw를 실행한 뒤 삼각형 문장을 추가한다. draw() { super.draw(); write(`삼각형을 그린다.`, "h3"); } } // Circle은 Shape를 상속받는다. class Circle extends Shape { // 원은 radius와 color가 필요하다. constructor(radius, color) { // 부모 constructor에 color를 전달한다. super(color); // 원만의 반지름을 저장한다. this.radius = radius; } // 원 넓이를 계산한다. getArea() { return this.radius * this.radius * Math.PI; } // 부모 draw를 실행한 뒤 원 문장을 추가한다. draw() { super.draw(); write(`원을 그린다.`, "h3"); } } // 사각형 객체를 만든다. const rectangle = new Rectangle(20, 20, "파랑"); rectangle.draw(); writeColor(rectangle.getArea(), "h2", "blue"); // 화면에 구분선을 출력한다. hr(); // 삼각형 객체를 만든다. const triangle = new Triangle(20, 20, "빨강"); triangle.draw(); writeColor(triangle.getArea(), "h2", "red"); // 화면에 구분선을 출력한다. hr(); // 원 객체를 만든다. const circle = new Circle(20, "초록"); circle.draw(); writeColor(circle.getArea(), "h2", "green"); // 화면에 구분선을 출력한다. hr(); // 객체를 그대로 화면에 출력한다. write(rectangle, "h1"); write(triangle, "h1"); write(circle, "h1"); </script> </body> </html>
exam15_1.html은Shape의 역할을 더 공통적인 구조로 바꾼다.
Shape는 모든 도형이 공통으로 가지는color만 저장한다.
그리고 각 도형의 넓이 계산에 필요한 값은 자식 클래스가 직접 저장한다.
Rectangle과Triangle은width,height가 필요하다.
그래서 각자의constructor에서this.width,this.height를 저장한다.
Circle은 가로와 세로가 아니라 반지름이 필요하다.
그래서radius를 받아this.radius에 저장한다.
이 구조가 더 자연스러운 이유는 도형마다 넓이를 구하는 데 필요한 값이 다르기 때문이다.
부모 클래스는 모든 도형에 공통인 색상만 관리하고, 각 자식 클래스는 자기 도형에 필요한 값을 따로 관리한다.
부모 클래스에는 공통 값과 공통 기능을 두고, 자식 클래스에는 도형별로 필요한 값을 따로 저장하는 것이 상속 구조를 더 자연스럽게 만든다.
예제 전체 코드 3
// exam15_2.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>Insert title here</title> </head> <body> <h1>자바스크립트의 객체 생성과 사용(클래스)</h1> <hr /> <!-- 화면 출력용 보조 함수 파일을 불러온다. --> <script src="../util.js"></script> <script> // Shape 클래스는 모든 도형이 공통으로 가지는 색상과 draw 기능을 가진다. class Shape { // color 값을 받아 현재 객체에 저장한다. constructor(color) { this.color = color; } // 객체를 문자열로 표현할 때 사용할 함수를 toString 프로퍼티에 저장한다. toString = () => "우리는 모두 도형"; // 도형의 공통 그리기 문장을 출력한다. draw() { write(`${this.color} 색상으로 도형을 그린다.`, "h3"); } } // Rectangle은 Shape를 상속받는다. class Rectangle extends Shape { constructor(width, height, color) { super(color); this.width = width; this.height = height; } getArea() { return this.width * this.height; } draw() { super.draw(); write(`사각형을 그린다.`, "h3"); } } // Triangle은 Shape를 상속받는다. class Triangle extends Shape { constructor(width, height, color) { super(color); this.width = width; this.height = height; } getArea() { return (this.width * this.height) / 2; } draw() { super.draw(); write(`삼각형을 그린다.`, "h3"); } } // Circle은 Shape를 상속받는다. class Circle extends Shape { constructor(radius, color) { super(color); this.radius = radius; } getArea() { return this.radius * this.radius * Math.PI; } draw() { super.draw(); write(`원을 그린다.`, "h3"); } } // 사각형 객체를 만든다. const rectangle = new Rectangle(20, 20, "파랑"); rectangle.draw(); writeColor(rectangle.getArea(), "h2", "blue"); // 화면에 구분선을 출력한다. hr(); // 삼각형 객체를 만든다. const triangle = new Triangle(20, 20, "빨강"); triangle.draw(); writeColor(triangle.getArea(), "h2", "red"); // 화면에 구분선을 출력한다. hr(); // 원 객체를 만든다. const circle = new Circle(20, "초록"); circle.draw(); writeColor(circle.getArea(), "h2", "green"); // 화면에 구분선을 출력한다. hr(); // 객체를 문자열로 출력하면 toString 프로퍼티에 저장된 함수의 반환값이 사용된다. write(rectangle, "h3"); write(triangle, "h3"); write(circle, "h3"); // 화면에 구분선을 출력한다. hr(); // 객체가 특정 클래스의 인스턴스인지 확인한다. write(rectangle instanceof Rectangle, "h3"); write(rectangle instanceof Shape, "h3"); write(rectangle instanceof Object, "h3"); write(rectangle instanceof Circle, "h3"); </script> </body> </html>
exam15_2.html은exam15_1.html에toString과instanceof확인을 추가한 예제이다.
Shape클래스 안에toString = () => "우리는 모두 도형";이 추가된다.
여기서toString = () => ...은 일반 메서드 문법인toString() { ... }이 아니다.
클래스 필드에 화살표 함수를 저장한 형태이다.
즉,toString이라는 이름의 프로퍼티에 객체를 문자열로 표현할 때 사용할 함수가 들어간다고 이해하면 된다.
Rectangle,Triangle,Circle은 모두Shape를 상속받는다.
그래서 세 객체를 문자열처럼 출력할 때toString프로퍼티에 저장된 함수의 반환값인"우리는 모두 도형"이 사용될 수 있다.
마지막에는instanceof를 사용한다.
instanceof는 어떤 객체가 특정 클래스에서 만들어졌는지 확인하는 연산자이다.
rectangle instanceof Rectangle은rectangle이Rectangle클래스의 객체인지 확인한다.
rectangle instanceof Shape는Rectangle이Shape를 상속받았기 때문에true가 된다.
상속받은 객체는 자기 클래스의 인스턴스이면서 동시에 부모 클래스의 인스턴스로도 볼 수 있다.
이 예제에서 확인할 핵심
extends는 부모 클래스를 상속받을 때 사용한다.Shape는 부모 클래스이다.Rectangle,Triangle,Circle은 자식 클래스이다.- 자식 클래스는 부모 클래스의 메서드를 사용할 수 있다.
- 자식 클래스에서 같은 이름의 메서드를 다시 만들면 부모 메서드를 재정의한다.
super()는 부모 클래스의 생성자를 호출한다.super.draw()는 부모 클래스의draw()메서드를 호출한다.super.getArea()는 부모 클래스의getArea()메서드를 호출한다.getArea()는 도형별 넓이를 계산하는 메서드이다.toString = () => ...은toString프로퍼티에 화살표 함수를 저장한 형태이다.instanceof는 객체가 특정 클래스의 인스턴스인지 확인한다.이 예제는 상속을 사용해 공통 기능을 부모 클래스에 두고, 자식 클래스에서 도형별 기능을 다르게 구현하는 흐름을 보여 준다.
Shape 클래스는 공통 부모 역할을 한다
// exam15.html // Shape 클래스는 도형의 공통 정보를 가진 부모 클래스이다. class Shape { // width, height, color 값을 받아 현재 객체에 저장한다. constructor(width, height, color) { this.width = width; this.height = height; this.color = color; } // 도형의 색상 정보를 화면에 출력한다. draw() { write(`${this.color} 색상으로 도형을 그린다.`, "h3"); } // 기본 넓이는 가로 * 세로로 계산한다. getArea() { return this.width * this.height; } }
Shape는 도형의 공통 부모 클래스이다.
이 코드에서는width,height,color를 저장한다.
그리고 색상으로 도형을 그린다는 문장을 출력하는draw()메서드와 넓이를 계산하는getArea()메서드를 가진다.
부모 클래스에 이렇게 공통 기능을 만들어 두면 자식 클래스가 그 기능을 물려받아 사용할 수 있다.
예를 들어Rectangle은 별도로draw()를 만들지 않아도Shape의draw()를 사용할 수 있다.
부모 클래스는 여러 자식 클래스가 함께 사용할 공통 프로퍼티와 메서드를 모아 두는 역할을 한다.
Rectangle은 부모 기능을 그대로 사용한다
// exam15.html // Rectangle은 Shape를 상속받는다. // 별도 constructor가 없으므로 Shape의 constructor 흐름을 그대로 사용한다. class Rectangle extends Shape {} const rectangle = new Rectangle(20, 20, "파랑"); rectangle.draw(); writeColor(rectangle.getArea(), "h2", "blue");
class Rectangle extends Shape {}는Rectangle이Shape를 상속받는다는 뜻이다.
여기서Rectangle클래스 안에는 별도 코드가 없다.
그런데도new Rectangle(20, 20, "파랑")으로 객체를 만들 수 있다.
자식 클래스에constructor가 없으면 부모 클래스의constructor흐름을 사용할 수 있기 때문이다.
또rectangle.draw()와rectangle.getArea()도 사용할 수 있다.
Rectangle이 부모 클래스인Shape의 메서드를 물려받았기 때문이다.
넓이는20 * 20이므로400이 된다.
화면 출력 흐름은 아래처럼 볼 수 있다.// 출력결과 // 파랑 색상으로 도형을 그린다. // 400자식 클래스가 별도로 메서드를 만들지 않으면 부모 클래스의 메서드를 그대로 사용할 수 있다.
Triangle은 부모 기능을 재정의한다
// exam15.html // Triangle은 Shape를 상속받는다. class Triangle extends Shape { // 삼각형 넓이는 부모의 넓이 계산 결과를 2로 나눈다. getArea() { return super.getArea() / 2; } // 부모의 draw를 먼저 실행한 뒤 삼각형 문장을 추가로 출력한다. draw() { super.draw(); write(`삼각형을 그린다.`, "h3"); } } const triangle = new Triangle(20, 20, "빨강"); triangle.draw(); writeColor(triangle.getArea(), "h2", "red");
Triangle도Shape를 상속받는다.
하지만Rectangle과 다르게getArea()와draw()를 직접 다시 작성한다.
getArea()에서는super.getArea()를 호출한다.
super.getArea()는 부모 클래스인Shape의getArea()를 실행한다.
부모의getArea()는width * height를 반환하므로20 * 20, 즉400을 만든다.
삼각형 넓이는 이 값의 절반이므로super.getArea() / 2는200이 된다.
draw()에서는super.draw()를 먼저 실행한다.
그래서"빨강 색상으로 도형을 그린다."가 먼저 출력된다.
그다음"삼각형을 그린다."가 추가로 출력된다.
화면 출력 흐름은 아래처럼 볼 수 있다.// 출력결과 // 빨강 색상으로 도형을 그린다. // 삼각형을 그린다. // 200자식 클래스는 부모 메서드를 그대로 쓰는 것뿐 아니라, 같은 이름의 메서드를 다시 정의해서 자기 방식으로 바꿀 수 있다.
자식 constructor에서 super를 호출하는 이유
// exam15_1.html class Rectangle extends Shape { constructor(width, height, color) { // 부모 constructor에 color를 전달한다. super(color); // 자식 클래스에서 필요한 값을 저장한다. this.width = width; this.height = height; } }
exam15_1.html에서는 자식 클래스에constructor가 있다.
자식 클래스가 직접constructor를 만들면 부모 생성자를 호출하기 위해super()를 사용해야 한다.
여기서 부모 클래스Shape는constructor(color)를 가진다.
그래서 자식 클래스는super(color)로 색상 값을 부모에게 전달한다.
부모 생성자는 그 값을 받아this.color = color로 저장한다.
그다음 자식 클래스는 자기에게 필요한width,height를 저장한다.
이렇게 하면 색상은 부모 쪽에서 관리하고, 가로와 세로는 자식 쪽에서 관리하는 구조가 된다.
자식 클래스의constructor에서는 부모 클래스 초기화를 위해super()를 먼저 호출한 뒤, 자식 클래스만의 값을 저장한다.
도형별 넓이 계산 방식 비교하기
exam15_1.html에서는 사각형, 삼각형, 원이 각각 자기 방식으로 넓이를 계산한다.
도형마다 넓이 공식이 다르기 때문이다.// exam15_1.html // 사각형 넓이이다. getArea() { return this.width * this.height; } // 삼각형 넓이이다. getArea() { return (this.width * this.height) / 2; } // 원 넓이이다. getArea() { return this.radius * this.radius * Math.PI; }사각형은 가로와 세로를 곱한다.
20 * 20이므로 넓이는400이다.
삼각형은 가로와 세로를 곱한 뒤2로 나눈다.
(20 * 20) / 2이므로 넓이는200이다.
원은 반지름을 두 번 곱하고Math.PI를 곱한다.
반지름이20이므로20 * 20 * Math.PI가 된다.
결과는 대략1256.6370614359173이다.
화면 출력 흐름은 아래처럼 볼 수 있다.// 출력결과 // 파랑 색상으로 도형을 그린다. // 사각형을 그린다. // 400 // 빨강 색상으로 도형을 그린다. // 삼각형을 그린다. // 200 // 초록 색상으로 도형을 그린다. // 원을 그린다. // 1256.6370614359173같은
getArea()라는 이름을 사용해도, 자식 클래스마다 계산 방식은 다르게 만들 수 있다.
객체를 그대로 출력하면 기본 문자열이 나올 수 있다
// exam15_1.html // 객체를 그대로 화면에 출력한다. write(rectangle, "h1"); write(triangle, "h1"); write(circle, "h1");
exam15_1.html에서는 마지막에 객체 자체를write()로 출력한다.
하지만 이 코드에는 객체를 어떻게 문자열로 표현할지 정한 사용자 정의toString이 없다.
일반적으로 객체를 문자열로 바꾸면[object Object]처럼 보일 수 있다.
다만 실제 화면 출력 결과는util.js의write()가 내부에서 값을 어떻게 처리하는지에 따라 달라질 수 있다.
중요한 점은 객체 안의 값이 사라진 것이 아니라는 점이다.
객체는 여전히width,height,color,radius같은 값을 가지고 있다.
다만 객체를 사람이 읽기 좋은 문자열로 바꾸는 방법을 따로 정하지 않았을 뿐이다.
객체를 사람이 읽기 좋은 문자열로 출력하려면toString역할을 하는 함수를 정의해야 한다.
toString 프로퍼티로 객체 문자열 표현 바꾸기
// exam15_2.html class Shape { constructor(color) { this.color = color; } // 객체를 문자열로 표현할 때 사용할 함수를 toString 프로퍼티에 저장한다. toString = () => "우리는 모두 도형"; draw() { write(`${this.color} 색상으로 도형을 그린다.`, "h3"); } }
exam15_2.html에서는Shape안에toString이 추가된다.
여기서toString = () => "우리는 모두 도형";은 일반 메서드 문법이 아니다.
toString이라는 클래스 필드에 화살표 함수를 저장한 형태이다.
즉, 객체가 문자열로 표현될 때 사용할 함수를toString이라는 이름으로 가지고 있게 만드는 코드이다.
이 함수는 실행되면"우리는 모두 도형"이라는 문자열을 반환한다.
Rectangle,Triangle,Circle은 모두Shape를 상속받는다.
그래서 세 객체를write()로 출력하면 모두"우리는 모두 도형"으로 출력될 수 있다.
화면 출력 흐름은 아래처럼 볼 수 있다.// 출력결과 // 우리는 모두 도형 // 우리는 모두 도형 // 우리는 모두 도형부모 클래스에
toString역할을 하는 함수를 정의하면, 그 부모를 상속받은 자식 객체들도 같은 문자열 표현을 사용할 수 있다.
instanceof로 객체의 소속 확인하기
// exam15_2.html // 객체가 특정 클래스의 인스턴스인지 확인한다. write(rectangle instanceof Rectangle, "h3"); write(rectangle instanceof Shape, "h3"); write(rectangle instanceof Object, "h3"); write(rectangle instanceof Circle, "h3");
instanceof는 객체가 특정 클래스에서 만들어졌는지 확인하는 연산자이다.
결과는true또는false로 나온다.
rectangle instanceof Rectangle은rectangle이Rectangle클래스로 만들어졌는지 확인한다.
맞으므로true이다.
rectangle instanceof Shape도true이다.
Rectangle이Shape를 상속받기 때문이다.
즉,rectangle은Rectangle객체이면서 동시에Shape계열의 객체라고 볼 수 있다.
rectangle instanceof Object도true이다.
JavaScript의 객체는 기본적으로Object를 바탕으로 만들어지기 때문이다.
반면rectangle instanceof Circle은false이다.
rectangle은 사각형 객체이지 원 객체가 아니기 때문이다.
화면 출력 결과는 아래와 같다.// 출력결과 // true // true // true // false상속 관계가 있으면 자식 객체는 자기 클래스뿐 아니라 부모 클래스의 인스턴스로도 판단될 수 있다.
상속 구조 전체 흐름 정리하기
이 예제의 상속 구조는 아래처럼 볼 수 있다.
Shape는 공통 부모 클래스이다.Rectangle은Shape를 상속받는다.Triangle은Shape를 상속받는다.Circle은Shape를 상속받는다.Shape는 공통 색상과 공통draw()흐름을 가진다.- 각 자식 클래스는 자기 도형에 필요한 값을 저장한다.
- 각 자식 클래스는 자기 방식으로
getArea()를 만든다.- 필요한 경우 자식 클래스는
draw()를 재정의한다.super.draw()를 사용하면 부모의draw()를 먼저 실행할 수 있다.toString역할을 하는 함수를 정의하면 객체의 문자열 표현을 바꿀 수 있다.instanceof로 객체가 어느 클래스 계열에 속하는지 확인할 수 있다.부모 클래스가 공통 기능을 담당하고, 자식 클래스가 세부 기능을 담당하면 코드 중복을 줄일 수 있다.
또 도형이 추가되어도 공통 기능은 부모에서 계속 사용할 수 있다.
상속은 공통 기능과 개별 기능을 나누어 클래스 구조를 더 깔끔하게 만드는 방법이다.
핵심 정리
이 예제의 핵심은
class상속을 사용해 공통 기능과 개별 기능을 나누는 것이다.
Shape는 공통 부모 클래스이고,Rectangle,Triangle,Circle은 각각의 도형을 표현하는 자식 클래스이다.
이 흐름은 아래처럼 정리할 수 있다.
extends는 부모 클래스를 상속받을 때 사용한다.Shape는 부모 클래스이다.Rectangle,Triangle,Circle은 자식 클래스이다.- 자식 클래스는 부모 클래스의 프로퍼티와 메서드를 사용할 수 있다.
- 자식 클래스에
constructor가 있으면super()로 부모 생성자를 먼저 호출한다.super.draw()는 부모 클래스의draw()를 실행한다.super.getArea()는 부모 클래스의getArea()를 실행한다.- 자식 클래스에서 부모와 같은 이름의 메서드를 다시 만들면 메서드가 재정의된다.
Rectangle의 넓이는width * height이다.Triangle의 넓이는(width * height) / 2이다.Circle의 넓이는radius * radius * Math.PI이다.toString = () => ...은toString프로퍼티에 화살표 함수를 저장한 형태이다.toString역할을 하는 함수를 정의하면 객체를 문자열로 출력할 때 사용할 표현을 바꿀 수 있다.instanceof는 객체가 특정 클래스의 인스턴스인지 확인한다.따라서 이 예제가 알려 주는 핵심은 이것이다.
상속은 부모 클래스의 공통 기능을 자식 클래스가 물려받게 하고, 자식 클래스에서는 필요한 기능을 추가하거나 재정의해서 객체 구조를 더 체계적으로 만드는 문법이다.
이 예제는
class안에서static멤버와static블록이 언제 실행되는지 확인하는 예제이다.
static은 객체마다 따로 만들어지는 값이 아니라, 클래스 자체에 붙는 값이나 기능을 만들 때 사용한다.
이 예제의 제목에는Spread연산자라고 적혀 있지만, 실제 코드에서 확인하는 핵심은Spread가 아니다.
실제 핵심은static프로퍼티,static메서드,static블록, 그리고constructor실행 순서이다.
이 예제의 핵심은static은 클래스 기준으로 동작하고,constructor는 객체 생성 기준으로 동작한다는 차이를 이해하는 것이다.
static이란 무엇인가
static은 클래스 자체에 붙는 멤버를 만들 때 사용하는 키워드이다.
여기서 멤버는 클래스 안에 들어 있는 프로퍼티나 메서드를 말한다.
일반 프로퍼티와 일반 메서드는new로 객체를 만든 뒤 객체를 통해 사용한다.
반면static프로퍼티와static메서드는 객체를 만들지 않아도 클래스 이름으로 바로 사용할 수 있다.
이 예제에서는 클래스 이름이ClassWithSTaticMethod이다.
따라서static멤버는 아래처럼 클래스 이름으로 접근한다.
ClassWithSTaticMethod.staticPClassWithSTaticMethod.staticM()ClassWithSTaticMethod.num
static멤버는 객체에 붙는 것이 아니라 클래스 자체에 붙는다.
그래서new로 객체를 만들지 않아도 클래스 이름으로 접근할 수 있다.
static 블록이란 무엇인가
static블록은 클래스가 처음 준비될 때 실행되는 코드 구간이다.
객체를 만들 때마다 실행되는 코드가 아니다.
이 예제에서는static { ... }안에서console.log()가 두 번 실행된다.
첫 번째console.log()는num값을 출력하고 증가시킨다.
두 번째console.log()는Math.random()으로 만든 임의의 숫자를 출력한다.
static블록은 클래스가 읽히는 시점에 한 번 실행된다.
반면constructor는new ClassWithSTaticMethod()로 객체를 만들 때마다 실행된다.
static블록은 클래스 기준으로 한 번 실행되고,constructor는 객체 생성 기준으로 매번 실행된다.
예제 전체 코드
// exam16.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> </head> <body> <h1>Spread 연산자</h1> <hr> <script src="../util.js"></script> <script> // static 멤버와 static 블록을 확인하기 위한 클래스이다. class ClassWithSTaticMethod { // static 프로퍼티이다. 클래스 이름으로 접근한다. static staticP = '정적데이터'; // static 프로퍼티이다. static 블록과 constructor에서 증가시킨다. static num = 1; // static 메서드 역할을 하는 화살표 함수이다. static staticM = () => { // 현재 날짜와 시간 정보를 가진 Date 객체를 만든다. const d = new Date(); // 날짜 정보를 문자열로 바꿔 반환한다. return d.toDateString(); } // static 블록이다. 클래스가 준비될 때 한 번 실행된다. static { // 현재 num 값을 출력한 뒤 1 증가시킨다. console.log("스태틱 블럭 수행 : " + ClassWithSTaticMethod.num++); // 0 이상 1 미만의 임의 숫자를 콘솔에 출력한다. console.log(Math.random()); } // constructor는 new로 객체를 만들 때마다 실행된다. constructor() { // 현재 num 값을 출력한 뒤 1 증가시킨다. console.log("생성자 수행 : " + ClassWithSTaticMethod.num++); } } // static 프로퍼티 staticP 값을 화면에 출력한다. write(ClassWithSTaticMethod.staticP, 'h3'); // staticM을 클래스 이름으로 호출하고, 반환된 날짜 문자열을 화면에 출력한다. write(ClassWithSTaticMethod.staticM(), 'h3'); // 객체를 하나 만든다. 이때 constructor가 실행된다. write(new ClassWithSTaticMethod(), 'h3'); // 객체를 하나 더 만든다. 이때 constructor가 다시 실행된다. write(new ClassWithSTaticMethod(), 'h3'); // static 프로퍼티 num의 최종 값을 화면에 출력한다. write(ClassWithSTaticMethod.num, 'h3'); </script> </body> </html>이 코드는 크게 다섯 부분으로 나누어 볼 수 있다.
첫 번째는static staticP로 정적 데이터를 저장하는 부분이다.
두 번째는static num으로 클래스 전체가 공유하는 숫자 값을 저장하는 부분이다.
세 번째는static staticM으로 클래스 이름으로 호출할 수 있는 함수를 만드는 부분이다.
네 번째는static블록이 클래스 준비 시점에 한 번 실행되는 부분이다.
다섯 번째는new ClassWithSTaticMethod()로 객체를 만들 때constructor가 실행되는 부분이다.
이 예제에서 가장 중요한 흐름은 실행 순서이다.
static블록은 객체를 만들기 전에 먼저 실행된다.
그다음new ClassWithSTaticMethod()가 실행될 때마다constructor가 실행된다.
static블록은 객체 생성 전에 한 번 실행되고,constructor는 객체를 만들 때마다 실행된다.
이 예제에서 확인할 핵심
static staticP는 클래스 자체가 가진 정적 프로퍼티이다.static num도 클래스 자체가 가진 정적 프로퍼티이다.static staticM = () => { ... }은 클래스 자체에 저장된 화살표 함수이다.static블록은 클래스가 준비될 때 한 번 실행된다.constructor는new로 객체를 만들 때마다 실행된다.ClassWithSTaticMethod.staticP는 클래스 이름으로static프로퍼티를 읽는 코드이다.ClassWithSTaticMethod.staticM()은 클래스 이름으로static함수를 호출하는 코드이다.new ClassWithSTaticMethod()는 객체를 만들고constructor를 실행한다.ClassWithSTaticMethod.num++은 현재 값을 사용한 뒤 값을1증가시킨다.이 예제는 클래스 자체에 붙은
static멤버와 객체 생성 시 실행되는constructor의 차이를 확인하는 예제이다.
static 프로퍼티 staticP 확인하기
// exam16.html // static 프로퍼티이다. 클래스 이름으로 접근한다. static staticP = '정적데이터'; write(ClassWithSTaticMethod.staticP, 'h3');
static staticP = '정적데이터';는staticP라는 프로퍼티를 클래스 자체에 만든다.
이 값은 객체마다 따로 생기는 값이 아니다.
그래서 값을 읽을 때 객체 이름을 사용하지 않는다.
클래스 이름인ClassWithSTaticMethod를 사용한다.
write(ClassWithSTaticMethod.staticP, 'h3')는ClassWithSTaticMethod클래스가 가진staticP값을 화면에 출력한다.
따라서 화면에는정적데이터가 출력된다.// 화면 출력 결과 // 정적데이터
static프로퍼티는객체명.프로퍼티명이 아니라클래스명.프로퍼티명으로 접근한다.
static 프로퍼티 num 확인하기
// exam16.html // static 프로퍼티이다. static 블록과 constructor에서 증가시킨다. static num = 1;
static num = 1;은 클래스 자체에num이라는 값을 만든다.
처음 값은1이다.
이 값은static블록과constructor에서 함께 사용된다.
중요한 점은num이 객체마다 따로 생기는 값이 아니라는 점이다.
ClassWithSTaticMethod클래스 자체가 가진 하나의 값이다.
그래서static블록에서 증가시키고, 객체를 만들 때constructor에서도 같은 값을 계속 증가시킨다.
static num은 모든 객체 생성 흐름에서 함께 사용하는 클래스 기준의 값이다.
staticM은 클래스 이름으로 호출하는 함수이다
// exam16.html // static 메서드 역할을 하는 화살표 함수이다. static staticM = () => { // 현재 날짜와 시간 정보를 가진 Date 객체를 만든다. const d = new Date(); // 날짜 정보를 문자열로 바꿔 반환한다. return d.toDateString(); } write(ClassWithSTaticMethod.staticM(), 'h3');
static staticM = () => { ... }은 클래스 자체에staticM이라는 이름의 함수를 저장한다.
정확히 말하면 일반 메서드 문법인static staticM() { ... }이 아니라,static클래스 필드에 화살표 함수를 저장한 형태이다.
이 함수 안에서는new Date()로 현재 날짜와 시간 정보를 가진Date객체를 만든다.
그다음d.toDateString()으로 날짜 정보를 문자열로 바꾸어 반환한다.
write(ClassWithSTaticMethod.staticM(), 'h3')는staticM()을 클래스 이름으로 호출한다.
그리고 반환된 날짜 문자열을 화면에 출력한다.
날짜 문자열은 실행하는 날짜에 따라 달라진다.
예를 들어 실행 날짜에 따라 아래처럼 보일 수 있다.// 화면 출력 결과 예시 // Thu May 14 2026
static으로 만든 함수는 객체를 만들지 않아도 클래스 이름으로 바로 호출할 수 있다.
static 블록 실행 흐름
// exam16.html // static 블록이다. 클래스가 준비될 때 한 번 실행된다. static { // 현재 num 값을 출력한 뒤 1 증가시킨다. console.log("스태틱 블럭 수행 : " + ClassWithSTaticMethod.num++); // 0 이상 1 미만의 임의 숫자를 콘솔에 출력한다. console.log(Math.random()); }
static블록은 클래스가 준비될 때 한 번 실행된다.
이 예제에서는 객체를 만들기 전에 먼저 실행된다.
처음ClassWithSTaticMethod.num값은1이다.
ClassWithSTaticMethod.num++는 현재 값인1을 먼저 사용하고, 그다음 값을2로 증가시킨다.
그래서 콘솔에는 먼저 아래와 같은 문장이 출력된다.// 콘솔 출력 결과 // 스태틱 블럭 수행 : 1그다음
Math.random()결과가 출력된다.
Math.random()은0이상1미만의 임의 숫자를 만든다.
이 값은 실행할 때마다 달라진다.// 콘솔 출력 결과 예시 // 0.384729183...
static블록은 클래스가 준비될 때 한 번만 실행되므로, 객체를 여러 개 만들어도 반복 실행되지 않는다.
constructor 실행 흐름
// exam16.html // constructor는 new로 객체를 만들 때마다 실행된다. constructor() { // 현재 num 값을 출력한 뒤 1 증가시킨다. console.log("생성자 수행 : " + ClassWithSTaticMethod.num++); } write(new ClassWithSTaticMethod(), 'h3'); write(new ClassWithSTaticMethod(), 'h3');
constructor는 객체를 만들 때 자동으로 실행되는 특별한 메서드이다.
이 예제에서는new ClassWithSTaticMethod()가 두 번 실행된다.
그래서constructor도 두 번 실행된다.
static블록이 먼저 실행되면서num은1에서2가 되었다.
첫 번째 객체를 만들 때constructor가 실행된다.
이때ClassWithSTaticMethod.num++는 현재 값2를 출력한 뒤3으로 증가시킨다.
두 번째 객체를 만들 때constructor가 다시 실행된다.
이때는 현재 값3을 출력한 뒤4로 증가시킨다.
콘솔 출력 흐름은 아래처럼 볼 수 있다.// 콘솔 출력 결과 // 생성자 수행 : 2 // 생성자 수행 : 3
constructor는new로 객체를 만들 때마다 실행되므로, 객체를 두 번 만들면 두 번 실행된다.
num 값이 변하는 순서
이 예제에서
num값은 실행 순서에 따라 바뀐다.
처음 값은1이다.
먼저static블록이 실행된다.
이때num++때문에1이 출력되고, 실제 값은2가 된다.
그다음 첫 번째 객체를 만든다.
이때constructor가 실행되어2가 출력되고, 실제 값은3이 된다.
그다음 두 번째 객체를 만든다.
이때 다시constructor가 실행되어3이 출력되고, 실제 값은4가 된다.
마지막으로 아래 코드가 실행된다.// exam16.html // static 프로퍼티 num의 최종 값을 화면에 출력한다. write(ClassWithSTaticMethod.num, 'h3');그래서 화면에는 최종 값
4가 출력된다.// 화면 출력 결과 // 4흐름을 정리하면 아래와 같다.
- 처음
num은1이다.static블록에서1을 출력하고2가 된다.- 첫 번째
constructor에서2를 출력하고3이 된다.- 두 번째
constructor에서3을 출력하고4가 된다.- 마지막 화면 출력에서
4가 나온다.
num++은 현재 값을 먼저 사용한 뒤 값을 증가시키기 때문에 출력값과 증가 후 값이 다르다.
화면 출력과 콘솔 출력 구분하기
이 예제는 화면 출력과 콘솔 출력이 함께 나온다.
write()는 화면에 내용을 출력한다.
console.log()는 브라우저 개발자 도구의 콘솔에 내용을 출력한다.
화면에 출력되는 값은 아래 코드들에서 나온다.// exam16.html write(ClassWithSTaticMethod.staticP, 'h3'); write(ClassWithSTaticMethod.staticM(), 'h3'); write(new ClassWithSTaticMethod(), 'h3'); write(new ClassWithSTaticMethod(), 'h3'); write(ClassWithSTaticMethod.num, 'h3');첫 번째
write()는정적데이터를 출력한다.
두 번째write()는 현재 날짜 문자열을 출력한다.
세 번째와 네 번째write()는 새 객체를 만들어 출력한다.
이때 객체 문자열 표현은util.js의write()처리 방식과 객체의 기본 문자열 변환 방식에 따라 보일 수 있다.
마지막write()는 최종num값인4를 출력한다.
콘솔에 출력되는 값은static블록과constructor에서 나온다.// exam16.html console.log("스태틱 블럭 수행 : " + ClassWithSTaticMethod.num++); console.log(Math.random()); console.log("생성자 수행 : " + ClassWithSTaticMethod.num++);콘솔에서는
static블록 실행 문장, 임의 숫자, 생성자 실행 문장을 확인할 수 있다.
write()결과는 화면에서 확인하고,console.log()결과는 개발자 도구 콘솔에서 확인한다.
실행 결과 흐름 정리
이 예제는 화면 출력과 콘솔 출력이 나뉘어 나타난다.
먼저 콘솔에서는static블록이 객체 생성보다 먼저 실행되는 것을 확인할 수 있다.
콘솔 출력 흐름은 아래처럼 볼 수 있다.// 콘솔 출력 결과 예시 // 스태틱 블럭 수행 : 1 // 0 이상 1 미만의 임의 숫자 // 생성자 수행 : 2 // 생성자 수행 : 3
Math.random()값은 실행할 때마다 달라진다.
그래서 정확한 숫자를 고정해서 적으면 안 된다.
화면 출력 흐름은 아래처럼 볼 수 있다.// 화면 출력 결과 예시 // 정적데이터 // 실행 날짜 문자열 // 객체 출력 결과 // 객체 출력 결과 // 4실행 날짜 문자열은
ClassWithSTaticMethod.staticM()에서 반환된다.
객체 출력 결과는new ClassWithSTaticMethod()로 만든 객체를write()에 전달한 결과이다.
마지막4는ClassWithSTaticMethod.num의 최종 값이다.
이 예제에서 고정적으로 확인할 수 있는 핵심은 마지막num값이4가 된다는 점이다.
static블록에서 한 번 증가하고, 객체 생성 두 번으로 두 번 더 증가하기 때문이다.
static 멤버와 인스턴스 생성 비교하기
static멤버와 인스턴스 생성은 기준이 다르다.
static멤버는 클래스 자체를 기준으로 사용한다.
인스턴스 생성은new로 만들어지는 객체를 기준으로 동작한다.
이 예제에서는staticP,num,staticM이static멤버이다.
이 값들은 객체 이름으로 접근하지 않고 클래스 이름으로 접근한다.
반면new ClassWithSTaticMethod()는 실제 객체를 만드는 코드이다.
이때는constructor가 실행된다.
정리하면 아래처럼 볼 수 있다.
ClassWithSTaticMethod.staticP는 클래스 자체의 값이다.ClassWithSTaticMethod.staticM()은 클래스 자체에 저장된 함수 호출이다.ClassWithSTaticMethod.num은 클래스 자체가 가진 숫자 값이다.new ClassWithSTaticMethod()는 객체를 만드는 코드이다.constructor는 객체를 만들 때마다 실행된다.
static멤버는 클래스 기준이고,new로 만들어지는 객체는 인스턴스 기준이다.
핵심 정리
이 예제의 핵심은
static멤버와constructor의 실행 기준을 구분하는 것이다.
static멤버는 클래스 자체에 붙고,constructor는 객체를 만들 때 실행된다.
이 흐름은 아래처럼 정리할 수 있다.
static staticP = '정적데이터'는 클래스 자체가 가진 정적 프로퍼티이다.static num = 1은 클래스 자체가 가진 숫자 값이다.static staticM = () => { ... }은 클래스 자체에 저장된 화살표 함수이다.static블록은 클래스가 준비될 때 한 번 실행된다.console.log("스태틱 블럭 수행 : " + ClassWithSTaticMethod.num++)는 현재num을 출력한 뒤 증가시킨다.Math.random()은 실행할 때마다 달라지는 임의 숫자를 만든다.constructor는new ClassWithSTaticMethod()가 실행될 때마다 호출된다.- 객체를 두 번 만들기 때문에
constructor도 두 번 실행된다.ClassWithSTaticMethod.num은static블록과 생성자 실행을 거쳐 최종적으로4가 된다.write()는 화면 출력이고,console.log()는 콘솔 출력이다.따라서 이 예제가 알려 주는 핵심은 이것이다.
static은 클래스 자체에 속하는 멤버를 만들고,static블록은 클래스 준비 시 한 번 실행되며,constructor는new로 객체를 만들 때마다 실행된다.
이 예제는 배열, 문자열, 객체에서 필요한 값을 꺼내 변수에 바로 저장하는 구조 분해 할당을 보여 준다.
구조 분해 할당은 여러 값을 가진 자료에서 값을 하나씩 꺼내 변수에 나누어 담는 문법이다.
배열에서는 위치 순서대로 값이 변수에 들어간다.
문자열에서는 문자 순서대로 값이 변수에 들어간다.
객체에서는 프로퍼티 이름을 기준으로 값이 변수에 들어간다.
구조 분해 할당의 핵심은 배열, 문자열, 객체 안의 값을 직접 꺼내서 변수에 쉽게 담는 것이다.
이 예제에서는 배열 구조 분해,Rest문법, 문자열 구조 분해,Spread문법, 객체 구조 분해, 변수 이름 변경, 기본값 설정까지 한 번에 확인한다.
구조 분해 할당이란 무엇인가
구조 분해 할당은 배열이나 객체처럼 여러 값을 담고 있는 자료에서 필요한 값을 꺼내 변수에 저장하는 문법이다.
일반적으로 값을 꺼내려면 배열의 인덱스나 객체의 프로퍼티 이름을 직접 사용해야 한다.
예를 들어 배열에서 첫 번째 값을 꺼내려면ary[0]을 사용한다.
두 번째 값을 꺼내려면ary[1]을 사용한다.
하지만 구조 분해 할당을 사용하면 아래처럼 한 번에 값을 꺼낼 수 있다.// exam17.html // 배열의 값을 순서대로 num1, num2, num3에 나누어 담는다. let ary = [10, 20, 30]; const [num1, num2, num3] = ary;이 코드는
ary배열의 첫 번째 값10을num1에 저장한다.
두 번째 값20을num2에 저장한다.
세 번째 값30을num3에 저장한다.
배열 구조 분해 할당은 배열의 순서에 맞춰 값을 변수에 나누어 담는다.
예제 전체 코드
// exam17.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> </head> <body> <h1>구조 분해 할당(중요!!)</h1> <hr> <script src="../util.js"></script> <script> // 숫자 3개가 들어 있는 배열을 만든다. let ary = [10, 20, 30]; // 배열의 값을 순서대로 num1, num2, num3에 나누어 담는다. const [num1, num2, num3] = ary; // 각각의 변수에 들어간 값을 확인한다. console.log(num1); console.log(num2); console.log(num3); // 구분선을 콘솔에 출력한다. console.log("=".repeat(15)); // 배열의 앞쪽 값 2개만 num4, num5에 나누어 담는다. let [num4, num5] = ary; // num4, num5에 들어간 값을 확인한다. console.log(num4); console.log(num5); // 구분선을 콘솔에 출력한다. console.log("=".repeat(15)); // 첫 번째 값은 num6에 담고, 나머지 값은 num7 배열에 모은다. let [num6, ...num7] = ary; // num6과 num7에 들어간 값을 확인한다. console.log(num6); console.log(num7); console.log("=".repeat(15)); // 문자열을 준비한다. let str = "가나다라마"; // 문자열은 문자 단위로 반복할 수 있다. for (let ch of str) { console.log(ch); } // 문자열을 문자 단위로 펼쳐 배열로 만든다. let ary1 = [...str]; console.log(ary1); // 문자열의 앞쪽 문자 2개를 c1, c2에 나누어 담는다. const [c1, c2] = str; // 문자열의 앞쪽 문자 2개는 c3, c4에 담고, 나머지는 c5 배열에 모은다. const [c3, c4, ...c5] = str; // c1, c2 값을 확인한다. console.log(c1); console.log(c2); console.log("=".repeat(15)); // c3, c4, c5 값을 확인한다. console.log(c3); console.log(c4); console.log(c5); // 커피 정보를 가진 객체를 만든다. let coffee = { name: "Americano", price: 3000 }; // CASE1: 객체 프로퍼티 이름과 같은 변수명으로 값을 꺼낸다. console.log("CASE1"); let { name, price } = coffee; console.log(name); console.log(price); // CASE2: 객체 프로퍼티 값을 다른 변수명으로 꺼낸다. console.log("CASE2"); let { name: coffeeName, price: coffeePrice } = coffee; console.log(coffeeName); console.log(coffeePrice); // CASE3: 존재하지 않는 프로퍼티를 꺼내면 undefined가 된다. console.log("CASE3"); let { name: coffeeName1, caffeine: coffeeCaffeine } = coffee; console.log(coffeeName1); console.log(coffeeCaffeine); // CASE4: 존재하지 않는 프로퍼티에 기본값을 지정한다. console.log("CASE4"); let { name: coffeeName2, caffeine: coffeeCaffeine1 = 500 } = coffee; console.log(coffeeName2); console.log(coffeeCaffeine1); </script> </body> </html>이 코드는 크게 네 부분으로 나누어 볼 수 있다.
첫 번째는 배열 구조 분해 할당이다.
두 번째는 문자열을 반복하거나 펼쳐서 문자 단위로 다루는 부분이다.
세 번째는 문자열 구조 분해 할당이다.
네 번째는 객체 구조 분해 할당이다.
이 예제에서 중요한 점은 자료의 종류마다 값을 꺼내는 기준이 다르다는 것이다.
배열과 문자열은 순서 기준으로 값이 들어간다.
객체는 순서가 아니라 프로퍼티 이름 기준으로 값이 들어간다.
배열과 문자열은 순서 기준, 객체는 프로퍼티 이름 기준으로 구조 분해 할당이 이루어진다.
이 예제에서 확인할 핵심
- 배열 구조 분해 할당은 배열의 값을 순서대로 변수에 담는다.
- 필요한 값만 앞에서부터 꺼낼 수도 있다.
...변수명을 사용하면 남은 값을 배열로 모을 수 있다.- 문자열은 문자 단위로 반복할 수 있다.
[...str]은 문자열을 문자 단위로 펼쳐 배열로 만든다.- 문자열도 구조 분해 할당할 수 있다.
- 객체 구조 분해 할당은 프로퍼티 이름을 기준으로 값을 꺼낸다.
{name, price}는coffee.name,coffee.price값을 꺼낸다.{name: coffeeName}은name프로퍼티 값을coffeeName변수에 저장한다.- 존재하지 않는 프로퍼티를 꺼내면
undefined가 된다.- 존재하지 않는 프로퍼티에는 기본값을 지정할 수 있다.
이 예제는 배열, 문자열, 객체에서 값을 꺼내는 기준이 어떻게 다른지 비교해서 보여 준다.
배열 값을 순서대로 변수에 담기
// exam17.html // 숫자 3개가 들어 있는 배열을 만든다. let ary = [10, 20, 30]; // 배열의 값을 순서대로 num1, num2, num3에 나누어 담는다. const [num1, num2, num3] = ary; console.log(num1); console.log(num2); console.log(num3);
ary배열에는10,20,30이 들어 있다.
const [num1, num2, num3] = ary는 배열의 값을 순서대로 변수에 담는다.
값이 들어가는 흐름은 아래와 같다.
num1에는 첫 번째 값10이 들어간다.num2에는 두 번째 값20이 들어간다.num3에는 세 번째 값30이 들어간다.출력 결과는 아래와 같다.
// 출력결과 // 10 // 20 // 30배열 구조 분해 할당은 변수 이름이 아니라 변수의 위치를 기준으로 값이 들어간다.
배열에서 앞쪽 값만 꺼내기
// exam17.html // 배열의 앞쪽 값 2개만 num4, num5에 나누어 담는다. let [num4, num5] = ary; console.log(num4); console.log(num5);이번에는 변수 두 개만 준비했다.
배열에는 값이3개 있지만, 구조 분해 할당에서 변수는num4,num5두 개뿐이다.
그러면 앞에서부터 두 개의 값만 변수에 들어간다.
num4에는10이 들어가고,num5에는20이 들어간다.
세 번째 값30은 받을 변수가 없으므로 따로 저장되지 않는다.
출력 결과는 아래와 같다.// 출력결과 // 10 // 20구조 분해 할당에서는 필요한 개수만큼만 값을 꺼낼 수 있다.
Rest 문법으로 나머지 값 모으기
// exam17.html // 첫 번째 값은 num6에 담고, 나머지 값은 num7 배열에 모은다. let [num6, ...num7] = ary; console.log(num6); console.log(num7);
let [num6, ...num7] = ary는 첫 번째 값과 나머지 값을 나누어 받는다.
num6에는 배열의 첫 번째 값10이 들어간다.
...num7은 남은 값을 배열로 모은다.
첫 번째 값을 제외하고 남은 값은20,30이다.
그래서num7에는[20, 30]이 들어간다.
출력 결과는 아래와 같다.// 출력결과 // 10 // [20, 30]여기서
...num7은Rest문법이다.
Rest는 나머지 값을 하나의 배열로 모을 때 사용한다.
구조 분해 할당에서...를 사용하면 아직 변수에 담기지 않은 나머지 값들을 배열로 모을 수 있다.
문자열을 문자 단위로 반복하기
// exam17.html // 문자열을 준비한다. let str = "가나다라마"; // 문자열은 문자 단위로 반복할 수 있다. for (let ch of str) { console.log(ch); }문자열은 글자들이 순서대로 이어진 값이다.
"가나다라마"는"가","나","다","라","마"가 순서대로 들어 있는 문자열이다.
for...of를 사용하면 문자열을 문자 단위로 하나씩 꺼낼 수 있다.
이때ch에는 현재 꺼낸 문자가 들어간다.
출력 결과는 아래와 같다.// 출력결과 // 가 // 나 // 다 // 라 // 마문자열도 반복 가능한 값이기 때문에
for...of로 한 글자씩 꺼낼 수 있다.
Spread 문법으로 문자열을 배열로 만들기
// exam17.html // 문자열을 문자 단위로 펼쳐 배열로 만든다. let ary1 = [...str]; console.log(ary1);
[...str]은 문자열을 문자 단위로 펼쳐 배열로 만든다.
여기서...str은Spread문법이다.
Spread는 묶여 있는 값을 펼칠 때 사용한다.
문자열"가나다라마"를 펼치면"가","나","다","라","마"가 된다.
이 값들을 배열 안에 넣으면["가", "나", "다", "라", "마"]가 된다.
출력 결과는 아래와 같다.// 출력결과 // ['가', '나', '다', '라', '마']문자열에
Spread를 사용하면 문자열을 문자 배열로 바꿀 수 있다.
문자열도 구조 분해 할당할 수 있다
// exam17.html // 문자열의 앞쪽 문자 2개를 c1, c2에 나누어 담는다. const [c1, c2] = str; console.log(c1); console.log(c2);문자열도 순서가 있는 값이므로 구조 분해 할당을 할 수 있다.
const [c1, c2] = str은 문자열의 앞쪽 문자 두 개를 변수에 담는다.
값이 들어가는 흐름은 아래와 같다.
c1에는 첫 번째 문자"가"가 들어간다.c2에는 두 번째 문자"나"가 들어간다.출력 결과는 아래와 같다.
// 출력결과 // 가 // 나문자열 구조 분해도 배열과 비슷하게 순서 기준으로 동작한다.
문자열 구조 분해 할당은 문자열의 문자 순서를 기준으로 변수에 값이 들어간다.
문자열 구조 분해에서 나머지 문자 모으기
// exam17.html // 문자열의 앞쪽 문자 2개는 c3, c4에 담고, 나머지는 c5 배열에 모은다. const [c3, c4, ...c5] = str; console.log(c3); console.log(c4); console.log(c5);이번에는 문자열에서 앞의 두 문자는 따로 받고, 나머지 문자는 배열로 모은다.
c3에는 첫 번째 문자"가"가 들어간다.
c4에는 두 번째 문자"나"가 들어간다.
...c5는 남은 문자들을 배열로 모은다.
남은 문자는"다","라","마"이다.
그래서c5에는["다", "라", "마"]가 들어간다.
출력 결과는 아래와 같다.// 출력결과 // 가 // 나 // ['다', '라', '마']문자열에서도
Rest문법을 사용하면 남은 문자들을 배열로 모을 수 있다.
객체 구조 분해 할당의 기본 형태
// exam17.html // 커피 정보를 가진 객체를 만든다. let coffee = { name: "Americano", price: 3000 }; // CASE1: 객체 프로퍼티 이름과 같은 변수명으로 값을 꺼낸다. console.log("CASE1"); let { name, price } = coffee; console.log(name); console.log(price);객체 구조 분해 할당은 배열과 기준이 다르다.
배열은 순서 기준이지만, 객체는 프로퍼티 이름 기준이다.
let { name, price } = coffee는coffee객체에서name프로퍼티와price프로퍼티를 꺼낸다.
그리고 각각 같은 이름의 변수name,price에 저장한다.
값이 들어가는 흐름은 아래와 같다.
coffee.name값"Americano"가name변수에 들어간다.coffee.price값3000이price변수에 들어간다.출력 결과는 아래와 같다.
// 출력결과 // CASE1 // Americano // 3000객체 구조 분해 할당은 변수의 위치가 아니라 프로퍼티 이름을 기준으로 값을 꺼낸다.
객체 값을 다른 변수명으로 꺼내기
// exam17.html // CASE2: 객체 프로퍼티 값을 다른 변수명으로 꺼낸다. console.log("CASE2"); let { name: coffeeName, price: coffeePrice } = coffee; console.log(coffeeName); console.log(coffeePrice);객체의 프로퍼티 이름과 다른 변수명을 사용하고 싶을 때는
프로퍼티이름: 새변수명형태를 사용한다.
name: coffeeName은coffee.name값을 꺼내서coffeeName변수에 저장한다는 뜻이다.
price: coffeePrice는coffee.price값을 꺼내서coffeePrice변수에 저장한다는 뜻이다.
여기서 중요한 점은name이라는 변수를 새로 만드는 것이 아니라는 점이다.
값이 실제로 저장되는 변수는coffeeName이다.
price도 마찬가지로 값이 실제로 저장되는 변수는coffeePrice이다.
출력 결과는 아래와 같다.// 출력결과 // CASE2 // Americano // 3000객체 구조 분해에서
name: coffeeName은name값을coffeeName이라는 새 변수명으로 받는다는 뜻이다.
존재하지 않는 프로퍼티를 꺼내면 undefined가 된다
// exam17.html // CASE3: 존재하지 않는 프로퍼티를 꺼내면 undefined가 된다. console.log("CASE3"); let { name: coffeeName1, caffeine: coffeeCaffeine } = coffee; console.log(coffeeName1); console.log(coffeeCaffeine);
coffee객체에는name과price만 있다.
caffeine프로퍼티는 없다.
name: coffeeName1은 존재하는name값을 꺼내므로coffeeName1에는"Americano"가 들어간다.
하지만caffeine: coffeeCaffeine은 존재하지 않는caffeine값을 꺼내려고 한다.
존재하지 않는 프로퍼티의 값은undefined이다.
따라서coffeeCaffeine에는undefined가 들어간다.
출력 결과는 아래와 같다.// 출력결과 // CASE3 // Americano // undefined
undefined는 값이 정의되어 있지 않다는 뜻이다.
즉,coffee객체 안에caffeine이라는 프로퍼티가 없다는 의미이다.
객체 구조 분해에서 존재하지 않는 프로퍼티를 꺼내면 변수에는undefined가 들어간다.
존재하지 않는 프로퍼티에 기본값 지정하기
// exam17.html // CASE4: 존재하지 않는 프로퍼티에 기본값을 지정한다. console.log("CASE4"); let { name: coffeeName2, caffeine: coffeeCaffeine1 = 500 } = coffee; console.log(coffeeName2); console.log(coffeeCaffeine1);이번에도
coffee객체에는caffeine프로퍼티가 없다.
하지만 이번에는caffeine: coffeeCaffeine1 = 500처럼 기본값을 지정했다.
이 코드는caffeine프로퍼티가 있으면 그 값을coffeeCaffeine1에 저장한다.
하지만caffeine프로퍼티가 없으면 기본값500을coffeeCaffeine1에 저장한다.
현재coffee객체에는caffeine이 없으므로coffeeCaffeine1에는500이 들어간다.
출력 결과는 아래와 같다.// 출력결과 // CASE4 // Americano // 500객체 구조 분해 할당에서 기본값을 지정하면, 프로퍼티가 없을 때
undefined대신 기본값을 사용할 수 있다.
배열·문자열·객체 구조 분해 비교하기
배열, 문자열, 객체는 모두 구조 분해 할당을 사용할 수 있다.
하지만 값을 꺼내는 기준은 다르다.
배열은 순서 기준으로 값을 꺼낸다.
문자열도 문자 순서 기준으로 값을 꺼낸다.
객체는 프로퍼티 이름 기준으로 값을 꺼낸다.
정리하면 아래처럼 볼 수 있다.
[num1, num2] = ary는 배열의 첫 번째, 두 번째 값을 꺼낸다.[c1, c2] = str은 문자열의 첫 번째, 두 번째 문자를 꺼낸다.{name, price} = coffee는 객체의name,price프로퍼티 값을 꺼낸다.- 배열과 문자열은 위치가 중요하다.
- 객체는 프로퍼티 이름이 중요하다.
구조 분해 할당을 볼 때는 대상이 배열인지, 문자열인지, 객체인지 먼저 구분해야 한다.
전체 콘솔 출력 흐름
이 예제는 대부분
console.log()로 값을 확인한다.
전체 출력 흐름은 아래처럼 볼 수 있다.// 출력결과 // 10 // 20 // 30 // =============== // 10 // 20 // =============== // 10 // [20, 30] // =============== // 가 // 나 // 다 // 라 // 마 // ['가', '나', '다', '라', '마'] // 가 // 나 // =============== // 가 // 나 // ['다', '라', '마'] // CASE1 // Americano // 3000 // CASE2 // Americano // 3000 // CASE3 // Americano // undefined // CASE4 // Americano // 500배열 구조 분해에서는 숫자 배열의 값이 순서대로 출력된다.
문자열 관련 코드에서는 문자들이 하나씩 출력되고, 문자열을 펼쳐 만든 배열도 출력된다.
객체 구조 분해에서는CASE1부터CASE4까지 프로퍼티 이름, 변수명 변경, 없는 값, 기본값 처리를 차례대로 확인한다.
출력 결과를 보면 구조 분해 할당이 값을 어떤 기준으로 변수에 담는지 확인할 수 있다.
핵심 정리
이 예제의 핵심은 배열, 문자열, 객체에서 값을 꺼내 변수에 담는 구조 분해 할당이다.
배열과 문자열은 순서 기준으로 값을 꺼내고, 객체는 프로퍼티 이름 기준으로 값을 꺼낸다.
이 흐름은 아래처럼 정리할 수 있다.
- 구조 분해 할당은 여러 값을 가진 자료에서 필요한 값을 꺼내 변수에 담는 문법이다.
- 배열 구조 분해 할당은 순서대로 값이 들어간다.
[num1, num2, num3] = ary는10,20,30을 각각 변수에 담는다.[num6, ...num7] = ary는 첫 번째 값은num6에 담고, 나머지는num7배열에 모은다.- 문자열은
for...of로 문자 단위 반복이 가능하다.[...str]은 문자열을 문자 단위로 펼쳐 배열로 만든다.- 문자열도 구조 분해 할당할 수 있다.
- 객체 구조 분해 할당은 프로퍼티 이름 기준으로 값을 꺼낸다.
{name, price} = coffee는coffee.name,coffee.price를 꺼낸다.{name: coffeeName}은name값을coffeeName변수에 저장한다.- 존재하지 않는 프로퍼티를 꺼내면
undefined가 된다.- 기본값을 지정하면 프로퍼티가 없을 때 기본값을 사용할 수 있다.
따라서 이 예제가 알려 주는 핵심은 이것이다.
구조 분해 할당은 배열과 문자열에서는 순서대로, 객체에서는 프로퍼티 이름대로 값을 꺼내 변수에 바로 저장하게 해 주는 문법이다.
이 예제는
JavaScript파일에서 다른 파일의 값을 가져와 사용하는module기본 흐름을 보여 준다.
module은 코드를 파일 단위로 나누고, 필요한 값이나 함수를 다른 파일에서 가져와 사용할 수 있게 해 주는 방식이다.
이 예제에서 가장 먼저 봐야 할 부분은exam18.html과exam18_1.html의 차이이다.
두 파일은 모두mainmodule1.js를 불러온다.
하지만exam18.html은type="module"을 붙여서 불러오고,exam18_1.html은 일반script로 불러온다.
import와export를 사용하는 파일은 반드시type="module"로 실행해야 한다.
mainmodule1.js는submodule1.js에서coffee와getCoffee를 가져온다.
submodule1.js는coffee,getCoffee,setCoffee를 외부로 내보낸다.
이 흐름을 통해 파일을 나누고, 필요한 값만 가져와 사용하는module구조를 확인할 수 있다.
module이란 무엇인가
module은JavaScript코드를 파일 단위로 나누어 관리하는 방식이다.
코드가 많아지면 모든 내용을 하나의 파일에 작성하기 어렵다.
그래서 역할에 따라 파일을 나누고, 필요한 값이나 함수만 가져와 사용한다.
예를 들어 커피 이름을 관리하는 코드는submodule1.js에 둘 수 있다.
그리고 그 값을 실제로 사용하는 코드는mainmodule1.js에 둘 수 있다.
이때submodule1.js는 외부에서 사용할 수 있도록 값을 내보내야 한다.
이 작업을export라고 한다.
반대로mainmodule1.js는 다른 파일에서 내보낸 값을 가져와야 한다.
이 작업을import라고 한다.
정리하면 아래처럼 볼 수 있다.
export는 현재 파일의 값이나 함수를 다른 파일에서 사용할 수 있게 내보내는 문법이다.import는 다른 파일에서 내보낸 값이나 함수를 현재 파일로 가져오는 문법이다.module은 여러 파일을 연결해서 코드를 나누어 관리하는 방식이다.
module은 코드를 파일별로 나누고, 필요한 것만 주고받게 만드는 구조이다.
type="module"이 필요한 이유
import와export는 일반script에서 바로 사용할 수 없다.
브라우저가 해당script를module로 처리해야import와export문법을 이해할 수 있다.
그래서HTML에서module파일을 불러올 때는 아래처럼 작성해야 한다.// exam18.html <script src="./mainmodule1.js" type="module"> </script>
type="module"은 이script가module방식으로 실행된다는 뜻이다.
이 설정이 있어야mainmodule1.js안의import문법이 정상적으로 동작한다.
반대로 아래처럼type="module"이 없으면 일반script로 처리된다.// exam18_1.html <script src="./mainmodule1.js"></script>그런데
mainmodule1.js안에는import문이 있다.
일반script는import문을 처리할 수 없으므로 오류가 날 수 있다.
import를 사용하는 파일을 불러올 때는script태그에type="module"을 붙여야 한다.
예제 전체 코드 1
// exam18.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>ECMAScript6</title> </head> <body> <!-- type="module"을 붙여 mainmodule1.js를 모듈로 실행한다. --> <script src="./mainmodule1.js" type="module"> </script> </body> </html>
exam18.html은mainmodule1.js를 불러올 때type="module"을 붙인다.
이렇게 작성하면 브라우저는mainmodule1.js를module파일로 처리한다.
그래서mainmodule1.js안의import문법이 정상적으로 해석된다.
이 예제에서 실제 실행 기준으로 잡아야 하는 파일은exam18.html이다.
exam18.html처럼type="module"을 붙여야mainmodule1.js의import코드가 정상 실행된다.
예제 전체 코드 2
// exam18_1.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>ECMAScript6</title> </head> <body> <!-- 일반 script 방식으로 mainmodule1.js를 불러온다. --> <script src="./mainmodule1.js"></script> </body> </html>
exam18_1.html은mainmodule1.js를 일반script방식으로 불러온다.
하지만mainmodule1.js안에는import문법이 들어 있다.
일반script는import문법을 처리하는 용도가 아니다.
그래서 이 파일은module예제를 정상 실행하기 위한 완성 형태라기보다,type="module"이 없을 때 문제가 생길 수 있음을 보여 주는 비교 예제로 볼 수 있다.
import가 들어 있는 파일을 일반script로 불러오면module문법을 처리하지 못할 수 있다.
예제 전체 코드 3
// mainmodule1.js // submodule1.js에서 coffee와 getCoffee를 가져온다. import { coffee, getCoffee } from "./submodule1.js"; // 가져온 coffee 값을 콘솔에 출력한다. console.log(coffee); // 가져온 getCoffee 함수를 실행하고 반환값을 콘솔에 출력한다. console.log(getCoffee()); // setCoffee는 가져오지 않았기 때문에 현재 코드에서는 사용하지 않는다. // console.log(setCoffee("Latte"));
mainmodule1.js는submodule1.js에서coffee와getCoffee를 가져온다.
가져올 때는 중괄호{}안에 이름을 적는다.
여기서 중요한 점은 이름이 정확히 맞아야 한다는 것이다.
submodule1.js에서coffee라는 이름으로 내보낸 값은coffee라는 이름으로 가져온다.
getCoffee라는 이름으로 내보낸 함수는getCoffee라는 이름으로 가져온다.
마지막 줄의setCoffee사용 코드는 주석 처리되어 있다.
현재import문에서는setCoffee를 가져오지 않았기 때문이다.
따라서 이 코드를 주석 해제해서 사용하려면import목록에setCoffee도 함께 넣어야 한다.
named export로 내보낸 값은import { 이름 }형태로 정확한 이름을 지정해서 가져온다.
예제 전체 코드 4
// submodule1.js // 다른 파일에서 사용할 커피 이름을 변수로 저장한다. let coffee = "Americano"; // coffee 값을 대문자로 바꿔 반환하는 함수이다. let getCoffee = function () { return coffee.toUpperCase() }; // coffee 값을 새 값으로 바꾸는 함수이다. let setCoffee = function (newValue) { coffee = newValue }; // coffee, getCoffee, setCoffee를 외부 파일에서 사용할 수 있게 내보낸다. export { coffee, getCoffee, setCoffee };
submodule1.js는 실제 데이터를 가지고 있는 파일이다.
여기에는coffee,getCoffee,setCoffee가 있다.
coffee에는"Americano"문자열이 저장되어 있다.
getCoffee()는coffee.toUpperCase()결과를 반환한다.
toUpperCase()는 문자열을 대문자로 바꾸는 메서드이다.
그래서"Americano"는"AMERICANO"가 된다.
setCoffee(newValue)는coffee값을 새 값으로 바꾸는 함수이다.
하지만 이번mainmodule1.js에서는setCoffee를 가져오지 않았고, 실행 코드도 주석 처리되어 있다.
그래서 실제 실행 흐름에서는setCoffee가 동작하지 않는다.
마지막의export { coffee, getCoffee, setCoffee };는 세 값을 외부 파일에서 사용할 수 있게 내보낸다.
export { ... }안에 들어간 이름만 다른 파일에서import로 가져올 수 있다.
이 예제에서 확인할 핵심
module은JavaScript코드를 파일 단위로 나누어 관리하는 방식이다.export는 현재 파일의 값이나 함수를 밖으로 내보낸다.import는 다른 파일에서 내보낸 값이나 함수를 현재 파일로 가져온다.import를 사용하는script는type="module"로 불러와야 한다.exam18.html은type="module"이 있어서mainmodule1.js를module로 실행한다.exam18_1.html은type="module"이 없어서import문법 실행에 적합하지 않다.mainmodule1.js는submodule1.js에서coffee와getCoffee를 가져온다.submodule1.js는coffee,getCoffee,setCoffee를 내보낸다.getCoffee()는coffee.toUpperCase()결과를 반환한다.이 예제는
export로 내보낸 값을import로 가져오고,type="module"을 붙여야 모듈 문법이 동작한다는 점을 보여 준다.
exam18.html은 module 방식으로 실행한다
// exam18.html <script src="./mainmodule1.js" type="module"> </script>
exam18.html은script태그에type="module"을 붙였다.
이 설정 때문에 브라우저는mainmodule1.js를module파일로 실행한다.
이제mainmodule1.js안의import문이 정상적으로 동작할 수 있다.
그리고mainmodule1.js는submodule1.js에서 필요한 값을 가져온다.
실행 흐름은 아래처럼 볼 수 있다.
exam18.html이mainmodule1.js를module로 불러온다.mainmodule1.js가submodule1.js에서coffee,getCoffee를 가져온다.coffee값을 콘솔에 출력한다.getCoffee()실행 결과를 콘솔에 출력한다.
type="module"은HTML에서 모듈 파일을 실행하기 위한 필수 설정이다.
exam18_1.html은 왜 문제가 될 수 있는가
// exam18_1.html <script src="./mainmodule1.js"></script>이 코드는
mainmodule1.js를 일반script방식으로 불러온다.
겉으로 보기에는 파일을 불러오는 코드라서 문제없어 보일 수 있다.
하지만mainmodule1.js안에는 아래 코드가 있다.// mainmodule1.js import { coffee, getCoffee } from "./submodule1.js";
import는module문법이다.
일반script에서는import문법을 그대로 사용할 수 없다.
그래서exam18_1.html처럼type="module"없이 실행하면 오류가 날 수 있다.
파일 안에import가 있다면 그 파일을 불러오는script에는type="module"이 필요하다.
named export로 값 내보내기
// submodule1.js let coffee = "Americano"; let getCoffee = function () { return coffee.toUpperCase() }; let setCoffee = function (newValue) { coffee = newValue }; export { coffee, getCoffee, setCoffee };
submodule1.js에서는 세 가지를 만든다.
coffee는 문자열 값이다.
getCoffee는coffee를 대문자로 바꿔 반환하는 함수이다.
setCoffee는coffee값을 바꾸는 함수이다.
마지막 줄에서export { coffee, getCoffee, setCoffee };를 작성했다.
이렇게 여러 이름을 중괄호 안에 넣어 내보내는 방식을named export라고 이해하면 된다.
named export는 이름이 중요하다.
내보낸 이름과 가져오는 이름이 맞아야 한다.
named export는 여러 값을 이름별로 내보내고, 다른 파일에서 같은 이름으로 가져오게 하는 방식이다.
named import로 값 가져오기
// mainmodule1.js import { coffee, getCoffee } from "./submodule1.js";이 코드는
submodule1.js에서coffee와getCoffee를 가져온다.
중괄호{}안에 가져올 이름을 적는다.
submodule1.js는coffee,getCoffee,setCoffee를 내보냈다.
그중에서mainmodule1.js는coffee와getCoffee만 가져온다.
즉, 내보낸 것을 전부 가져와야 하는 것은 아니다.
필요한 것만 골라서 가져올 수 있다.
import { coffee, getCoffee }는 내보낸 값 중에서coffee와getCoffee만 가져오겠다는 뜻이다.
coffee 값 출력하기
// mainmodule1.js console.log(coffee);
coffee는submodule1.js에서 가져온 값이다.
submodule1.js에서coffee는"Americano"로 선언되어 있다.
따라서console.log(coffee)를 실행하면 콘솔에는"Americano"가 출력된다.// 콘솔 출력 결과 // Americano이 결과는
mainmodule1.js안에서 직접coffee를 만든 것이 아니다.
다른 파일인submodule1.js에서 내보낸 값을 가져와 출력한 결과이다.
import로 가져온 값은 현재 파일에서 선언한 값처럼 사용할 수 있다.
getCoffee 함수 실행하기
// mainmodule1.js console.log(getCoffee());
getCoffee도submodule1.js에서 가져온 함수이다.
이 함수는 아래처럼 작성되어 있다.// submodule1.js let getCoffee = function () { return coffee.toUpperCase() };
coffee값은"Americano"이다.
toUpperCase()는 문자열을 대문자로 바꾼다.
그래서getCoffee()의 반환값은"AMERICANO"가 된다.
따라서 콘솔 출력 결과는 아래와 같다.// 콘솔 출력 결과 // AMERICANO다른 파일에서 가져온 함수도 현재 파일의 함수처럼 호출할 수 있다.
setCoffee가 주석 처리된 이유
// mainmodule1.js // console.log(setCoffee("Latte"));이 줄은 주석 처리되어 있다.
현재mainmodule1.js의import문은 아래와 같다.// mainmodule1.js import { coffee, getCoffee } from "./submodule1.js";여기에는
setCoffee가 없다.
따라서setCoffee("Latte")를 실제로 실행하려면setCoffee도 가져와야 한다.
예를 들어 아래처럼 작성해야 사용할 수 있다.// mainmodule1.js // 비교용 예시이다. import { coffee, getCoffee, setCoffee } from "./submodule1.js";현재 원본 코드에서는
setCoffee를 사용하지 않기 때문에 주석 처리되어 있다.
export된 값이라도 현재 파일에서import하지 않으면 사용할 수 없다.
실행 결과 정리
exam18.html을 기준으로 실행하면mainmodule1.js가module로 실행된다.
그 결과submodule1.js에서coffee와getCoffee를 가져와 콘솔에 출력한다.
콘솔 출력 결과는 아래와 같다.// 콘솔 출력 결과 // Americano // AMERICANO첫 번째 줄은
console.log(coffee)의 결과이다.
coffee값 자체인"Americano"가 출력된다.
두 번째 줄은console.log(getCoffee())의 결과이다.
getCoffee()가coffee.toUpperCase()를 반환하므로"AMERICANO"가 출력된다.
이 결과는mainmodule1.js가submodule1.js에서 값을 가져와 사용했다는 것을 보여 준다.
module 기본 흐름 정리하기
이 예제의 전체 흐름은 아래처럼 볼 수 있다.
submodule1.js에서coffee,getCoffee,setCoffee를 만든다.export { coffee, getCoffee, setCoffee }로 세 이름을 내보낸다.exam18.html에서mainmodule1.js를type="module"로 불러온다.mainmodule1.js에서import { coffee, getCoffee }로 필요한 값만 가져온다.console.log(coffee)로"Americano"를 출력한다.console.log(getCoffee())로"AMERICANO"를 출력한다.이 흐름에서 가장 중요한 연결은
export와import이다.
submodule1.js가 내보내고,mainmodule1.js가 가져온다.
그리고HTML에서는type="module"을 붙여mainmodule1.js를 모듈로 실행한다.
module흐름은 내보내는 파일, 가져오는 파일, 모듈로 실행하는HTML설정이 함께 맞아야 정상 동작한다.
핵심 정리
이 예제의 핵심은
module을 사용해 파일 간에 값과 함수를 주고받는 방법이다.
submodule1.js는 값을 내보내고,mainmodule1.js는 그 값을 가져와 사용한다.
이 흐름은 아래처럼 정리할 수 있다.
export는 현재 파일의 값을 다른 파일에서 사용할 수 있게 내보낸다.import는 다른 파일이 내보낸 값을 현재 파일로 가져온다.named export는 여러 값을 이름별로 내보내는 방식이다.named import는 중괄호{}안에 가져올 이름을 적는 방식이다.submodule1.js는coffee,getCoffee,setCoffee를 내보낸다.mainmodule1.js는 그중coffee,getCoffee만 가져온다.coffee는"Americano"값을 가진다.getCoffee()는"AMERICANO"를 반환한다.import를 사용하는 파일은type="module"로 불러와야 한다.exam18.html처럼type="module"이 있어야 모듈 코드가 정상 실행된다.exam18_1.html처럼type="module"이 없으면import문법 실행에 적합하지 않다.따라서 이 예제가 알려 주는 핵심은 이것이다.
export로 내보낸 값은import로 가져와 사용할 수 있고, 브라우저에서 이 구조를 실행하려면script태그에type="module"을 반드시 지정해야 한다.
이 예제는 다른 파일에서 내보낸 값을 가져올 때, 원래 이름 그대로 쓰지 않고 다른 이름으로 바꿔 사용하는 흐름을 보여 준다.
module에서는export한 이름을import로 가져올 수 있다.
그런데 가져온 이름이 너무 길거나, 현재 파일 안에서 다른 이름으로 쓰고 싶을 때가 있다.
이럴 때as를 사용한다.
as는 가져온 이름에 별칭을 붙이는 문법이다.
별칭은 원래 이름 대신 현재 파일 안에서 사용할 새 이름이라고 이해하면 된다.
이 예제의 핵심은named export로 내보낸 값을import할 때as로 이름을 바꿔 사용할 수 있다는 점이다.
앞 예제에서는coffee,getCoffee를 원래 이름 그대로 가져왔다.
이번 예제에서는coffee를x라는 이름으로 가져오고,getCoffee를y라는 이름으로 가져온다.
별칭이란 무엇인가
별칭은 원래 이름 대신 사용할 다른 이름이다.
JavaScript의module에서는 다른 파일에서 가져온 값이나 함수에 별칭을 붙일 수 있다.
예를 들어submodule1.js는coffee라는 값을 내보낸다.
그런데mainmodule2.js에서는 이 값을coffee라는 이름으로 쓰지 않고x라는 이름으로 사용한다.
아래 코드에서coffee as x가 바로 그 의미이다.// mainmodule2.js // submodule1.js에서 coffee를 가져오되 현재 파일에서는 x라는 이름으로 사용한다. import { coffee as x } from "./submodule1.js";이 코드는
submodule1.js에서 내보낸coffee값을 가져온다.
하지만mainmodule2.js안에서는 그 값을x라는 이름으로 사용한다.
as는 가져온 이름을 현재 파일 안에서 사용할 다른 이름으로 바꿔 주는 문법이다.
별칭이 필요한 이유
별칭이 필요한 이유는 이름 충돌을 피하거나, 현재 파일에서 더 짧은 이름으로 사용하기 위해서이다.
예를 들어 여러 파일에서 같은 이름의 값을 가져올 수 있다.
이때 이름이 같으면 어떤 값인지 헷갈릴 수 있다.
또 가져온 이름이 너무 길면 코드가 길어질 수 있다.
이런 경우as로 이름을 바꾸면 현재 파일 안에서 더 편하게 사용할 수 있다.
다만 별칭을 붙인 뒤에는 원래 이름이 아니라 새 이름을 사용해야 한다.
정리하면 아래처럼 볼 수 있다.
coffee as x는coffee를 가져와서x라는 이름으로 사용한다.getCoffee as y는getCoffee를 가져와서y라는 이름으로 사용한다.- 별칭을 붙인 뒤에는 현재 파일에서
coffee,getCoffee가 아니라x,y를 사용한다.별칭을 붙이면 가져온 값을 현재 파일 안에서 다른 이름으로 다룰 수 있다.
예제 전체 코드 1
// exam19.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>ECMAScript6</title> </head> <body> <!-- mainmodule2.js 안에서 import를 사용하므로 type="module"이 필요하다. --> <script src="./mainmodule2.js" type="module"></script> </body> </html>
exam19.html은mainmodule2.js를 실행하는HTML파일이다.
여기서 중요한 부분은script태그에type="module"이 붙어 있다는 점이다.
mainmodule2.js안에는import문법이 들어 있다.
따라서 일반script가 아니라module방식으로 실행해야 한다.
import가 들어 있는 파일을 브라우저에서 실행하려면script태그에type="module"을 붙여야 한다.
예제 전체 코드 2
// mainmodule2.js // submodule1.js에서 coffee와 getCoffee를 가져오되 현재 파일에서는 이름을 바꿔 사용한다. import { coffee as x, getCoffee as y } from "./submodule1.js"; // x는 submodule1.js의 coffee 값을 가리킨다. console.log(x); // y는 submodule1.js의 getCoffee 함수를 가리킨다. console.log(y());
mainmodule2.js는submodule1.js에서 값을 가져온다.
하지만 원래 이름 그대로 가져오지 않는다.
coffee as x는coffee값을 가져와서 현재 파일에서는x라는 이름으로 쓰겠다는 뜻이다.
getCoffee as y는getCoffee함수를 가져와서 현재 파일에서는y라는 이름으로 쓰겠다는 뜻이다.
그래서 아래 코드가 가능하다.
console.log(x)는coffee값을 출력한다.console.log(y())는getCoffee()실행 결과를 출력한다.별칭을 붙인 뒤에는 현재 파일에서 별칭 이름으로 값을 사용한다.
예제 전체 코드 3
// submodule1.js // 다른 파일에서 사용할 커피 이름을 변수로 저장한다. let coffee = "Americano"; // coffee 값을 대문자로 바꿔 반환하는 함수이다. let getCoffee = function () { return coffee.toUpperCase() }; // coffee 값을 새 값으로 바꾸는 함수이다. let setCoffee = function (newValue) { coffee = newValue }; // coffee, getCoffee, setCoffee를 외부 파일에서 사용할 수 있게 내보낸다. export { coffee, getCoffee, setCoffee };
submodule1.js는 값을 내보내는 파일이다.
이 파일은coffee,getCoffee,setCoffee를 만든 뒤export로 내보낸다.
이번 예제에서mainmodule2.js가 실제로 가져오는 것은coffee와getCoffee이다.
setCoffee도 내보내져 있지만, 이번 코드에서는 가져오지 않는다.
coffee값은"Americano"이다.
getCoffee()는coffee.toUpperCase()를 반환한다.
toUpperCase()는 문자열을 대문자로 바꾸는 메서드이다.
그래서"Americano"는"AMERICANO"가 된다.
별칭은 가져오는 쪽에서 붙이는 이름일 뿐, 내보내는 파일의 원래 이름을 바꾸는 것은 아니다.
이 예제에서 확인할 핵심
exam19.html은mainmodule2.js를type="module"로 실행한다.submodule1.js는coffee,getCoffee,setCoffee를export한다.mainmodule2.js는coffee와getCoffee만 가져온다.coffee as x는coffee값을 현재 파일에서x라는 이름으로 사용한다.getCoffee as y는getCoffee함수를 현재 파일에서y라는 이름으로 사용한다.- 별칭을 붙인 뒤에는 현재 파일에서 원래 이름이 아니라 별칭 이름을 사용한다.
console.log(x)는"Americano"를 출력한다.console.log(y())는"AMERICANO"를 출력한다.이 예제는
named import에서as를 사용해 가져온 이름을 바꿔 쓰는 방법을 보여 준다.
exam19.html에서 module로 실행하기
// exam19.html <!-- mainmodule2.js 안에서 import를 사용하므로 type="module"이 필요하다. --> <script src="./mainmodule2.js" type="module"></script>
exam19.html은mainmodule2.js를 불러온다.
이때type="module"을 붙인다.
그 이유는mainmodule2.js안에import문법이 있기 때문이다.
import는module문법이므로HTML에서module로 실행해야 한다.
만약type="module"이 없으면 브라우저가 일반script로 처리할 수 있다.
그러면import문법을 제대로 처리하지 못해 오류가 날 수 있다.
module파일을 실행하는 출발점은HTML의script type="module"설정이다.
coffee를 x라는 이름으로 가져오기
// mainmodule2.js // submodule1.js에서 coffee를 가져오되 현재 파일에서는 x라는 이름으로 사용한다. import { coffee as x } from "./submodule1.js"; console.log(x);
submodule1.js는coffee를 내보낸다.
mainmodule2.js는 이coffee를 가져오면서x라는 별칭을 붙인다.
그래서mainmodule2.js안에서는coffee라는 이름 대신x를 사용한다.
x가 가리키는 실제 값은submodule1.js의coffee이다.
submodule1.js에서coffee는"Americano"이다.
따라서console.log(x)의 결과는 아래와 같다.// 콘솔 출력 결과 // Americano
coffee as x를 사용하면 가져온coffee값을 현재 파일에서는x라는 이름으로 사용할 수 있다.
getCoffee를 y라는 이름으로 가져오기
// mainmodule2.js // submodule1.js에서 getCoffee를 가져오되 현재 파일에서는 y라는 이름으로 사용한다. import { getCoffee as y } from "./submodule1.js"; console.log(y());
submodule1.js는getCoffee함수도 내보낸다.
mainmodule2.js는 이 함수를 가져오면서y라는 별칭을 붙인다.
그래서mainmodule2.js안에서는getCoffee()라고 호출하지 않고y()라고 호출한다.
하지만 실제로 실행되는 함수는submodule1.js의getCoffee이다.
getCoffee()는coffee.toUpperCase()를 반환한다.
현재coffee는"Americano"이므로 결과는"AMERICANO"이다.
출력 결과는 아래와 같다.// 콘솔 출력 결과 // AMERICANO
getCoffee as y를 사용하면 가져온 함수를 현재 파일에서y()라는 이름으로 호출할 수 있다.
원래 이름과 별칭 이름 구분하기
// mainmodule2.js import { coffee as x, getCoffee as y } from "./submodule1.js"; console.log(x); console.log(y());이 코드에서 현재 파일 안에서 사용할 수 있는 이름은
x와y이다.
coffee와getCoffee는submodule1.js에서 내보낸 원래 이름이다.
즉,import문 안에서는 원래 이름이 필요하다.
하지만 가져온 뒤 실제 코드에서 사용할 때는 별칭 이름을 사용한다.
정리하면 아래처럼 볼 수 있다.
coffee는submodule1.js에서 내보낸 이름이다.x는mainmodule2.js에서 사용할 별칭 이름이다.getCoffee는submodule1.js에서 내보낸 이름이다.y는mainmodule2.js에서 사용할 별칭 이름이다.별칭을 붙인 뒤 현재 파일의 실행 코드에서는 원래 이름이 아니라 별칭 이름을 사용해야 한다.
as를 사용하지 않은 경우와 비교하기
앞 예제에서는 이름을 그대로 가져왔다.
// mainmodule1.js // 이름을 그대로 가져오는 방식이다. import { coffee, getCoffee } from "./submodule1.js"; console.log(coffee); console.log(getCoffee());이 방식에서는
submodule1.js에서 내보낸 이름을 그대로 사용한다.
그래서 현재 파일에서도coffee,getCoffee라고 작성한다.
이번 예제에서는 이름을 바꿔 가져온다.// mainmodule2.js // 가져온 이름에 별칭을 붙이는 방식이다. import { coffee as x, getCoffee as y } from "./submodule1.js"; console.log(x); console.log(y());출력 결과는 두 방식 모두 같다.
하지만 현재 파일에서 사용하는 이름이 달라진다.
정리하면 아래처럼 볼 수 있다.
- 이름을 그대로 가져오면
coffee,getCoffee를 그대로 사용한다.- 별칭을 붙이면
x,y처럼 바꾼 이름을 사용한다.- 출력 결과는 같을 수 있지만 코드에서 사용하는 이름은 달라진다.
as는 가져오는 값 자체를 바꾸는 것이 아니라, 현재 파일에서 부를 이름만 바꾸는 문법이다.
실행 결과 정리
exam19.html을 실행하면mainmodule2.js가module로 실행된다.
mainmodule2.js는submodule1.js에서coffee와getCoffee를 별칭으로 가져온다.
첫 번째 출력은console.log(x)에서 나온다.
x는coffee의 별칭이고,coffee값은"Americano"이다.
두 번째 출력은console.log(y())에서 나온다.
y는getCoffee의 별칭이다.
getCoffee()는coffee.toUpperCase()를 반환하므로"AMERICANO"가 출력된다.
콘솔 출력 결과는 아래와 같다.// 콘솔 출력 결과 // Americano // AMERICANO별칭을 사용해도 가져온 값과 함수의 동작 자체는 바뀌지 않는다.
바뀌는 것은 현재 파일 안에서 부르는 이름이다.
핵심 정리
이 예제의 핵심은
named import에서as를 사용해 가져온 이름을 바꾸는 방법이다.
submodule1.js는 원래 이름으로 값을 내보내고,mainmodule2.js는 그 이름을 가져오면서 별칭을 붙인다.
이 흐름은 아래처럼 정리할 수 있다.
submodule1.js는coffee,getCoffee,setCoffee를 내보낸다.mainmodule2.js는coffee와getCoffee를 가져온다.coffee as x는coffee를 현재 파일에서x라는 이름으로 사용한다는 뜻이다.getCoffee as y는getCoffee를 현재 파일에서y라는 이름으로 사용한다는 뜻이다.- 별칭을 붙인 뒤에는
x,y를 사용한다.x의 실제 값은"Americano"이다.y()의 실제 실행 결과는"AMERICANO"이다.as는 가져온 값 자체를 바꾸지 않고, 현재 파일에서 사용할 이름만 바꾼다.import문법을 사용하는 파일은type="module"로 실행해야 한다.따라서 이 예제가 알려 주는 핵심은 이것이다.
as는named export로 내보낸 값을 가져올 때 현재 파일에서 사용할 이름을 바꾸는 문법이며, 값 자체가 아니라 이름만 바뀐다.
이 예제는 다른 파일에서
export한 여러 값을 한 번에 묶어서 가져오는 방법을 보여 준다.
앞 예제에서는 필요한 값만 골라서 가져오거나, 가져온 값에 별칭을 붙였다.
이번에는import * as myCoffee형태로submodule1.js가 내보낸 값들을 하나의 이름 아래에 모아서 가져온다.
이렇게 가져오면coffee,getCoffee,setCoffee를 각각 따로 변수처럼 바로 쓰는 것이 아니다.
myCoffee.coffee,myCoffee.getCoffee(),myCoffee.setCoffee()처럼 묶음 이름을 앞에 붙여서 사용한다.
이 예제의 핵심은export된 여러 값을 하나의 모듈 객체처럼 묶어서 가져오고, 그 안의 값을 점 표기법으로 사용하는 것이다.
모듈 전체 가져오기란 무엇인가
모듈 전체 가져오기는 한 파일에서 내보낸 여러 값을 하나의 이름으로 묶어서 가져오는 방식이다.
문법은import * as 별칭 from "파일경로"형태이다.
이 예제에서는 아래처럼 작성한다.// mainmodule3.js // submodule1.js가 내보낸 값들을 myCoffee라는 이름으로 묶어서 가져온다. import * as myCoffee from "./submodule1.js";
*는 해당 모듈에서 내보낸 여러 값을 모두 가져온다는 뜻으로 이해하면 된다.
as myCoffee는 가져온 전체 묶음에myCoffee라는 이름을 붙인다는 뜻이다.
그래서submodule1.js에서 내보낸coffee,getCoffee,setCoffee는myCoffee안에 들어 있는 것처럼 사용할 수 있다.
import * as myCoffee는 내보낸 값들을myCoffee라는 묶음 이름 아래에 모아 가져오는 문법이다.
이전 import 방식과 다른 점
앞 예제에서는 필요한 이름을 중괄호 안에 직접 적었다.
// mainmodule1.js // 필요한 값만 이름 그대로 가져오는 방식이다. import { coffee, getCoffee } from "./submodule1.js";이 방식은 가져온 뒤
coffee,getCoffee()처럼 바로 사용할 수 있다.
또 이전 예제에서는as를 사용해 이름을 바꿔 가져왔다.// mainmodule2.js // 가져온 이름에 별칭을 붙이는 방식이다. import { coffee as x, getCoffee as y } from "./submodule1.js";이 방식은 가져온 뒤
x,y()처럼 별칭 이름으로 사용한다.
이번 예제는 다르다.// mainmodule3.js // export된 값들을 myCoffee라는 묶음으로 가져오는 방식이다. import * as myCoffee from "./submodule1.js";이 방식에서는
coffee를 바로 쓰지 않는다.
반드시myCoffee.coffee처럼 묶음 이름을 붙여 사용한다.
모듈 전체 가져오기는 값을 각각 독립 변수로 가져오는 것이 아니라, 하나의 묶음 객체 안에 넣어 사용하는 방식이다.
예제 전체 코드 1
// exam20.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>ECMAScript6</title> </head> <body> <!-- mainmodule3.js 안에서 import를 사용하므로 type="module"이 필요하다. --> <script src="./mainmodule3.js" type="module"></script> </body> </html>
exam20.html은mainmodule3.js를 실행하는HTML파일이다.
여기서도type="module"이 필요하다.
mainmodule3.js안에는import문법이 들어 있다.
따라서 브라우저가 이 파일을 일반script가 아니라module로 실행해야 한다.
import문법이 들어 있는 파일은script type="module"로 불러와야 정상 실행된다.
예제 전체 코드 2
// mainmodule3.js // submodule1.js에서 export한 값들을 myCoffee라는 이름으로 묶어서 가져온다. import * as myCoffee from "./submodule1.js"; // myCoffee 안의 coffee 값을 출력한다. console.log(myCoffee.coffee); // myCoffee 안의 getCoffee 함수를 실행하고 결과를 출력한다. console.log(myCoffee.getCoffee()); // myCoffee 안의 setCoffee 함수를 실행해 coffee 값을 Latte로 바꾼다. myCoffee.setCoffee("Latte"); // 변경된 coffee 값을 getCoffee 함수로 다시 확인한다. console.log(myCoffee.getCoffee());
mainmodule3.js는submodule1.js에서 내보낸 값들을myCoffee라는 이름으로 묶어 가져온다.
그래서coffee,getCoffee,setCoffee를 직접 쓰지 않고 앞에myCoffee.를 붙여 사용한다.
myCoffee.coffee는submodule1.js의coffee값을 가리킨다.
myCoffee.getCoffee()는submodule1.js의getCoffee함수를 실행한다.
myCoffee.setCoffee("Latte")는submodule1.js의coffee값을"Latte"로 바꾼다.
모듈 전체 가져오기를 사용하면 가져온 값에 접근할 때 항상묶음이름.내보낸이름형태를 사용한다.
예제 전체 코드 3
// submodule1.js // 다른 파일에서 사용할 커피 이름을 변수로 저장한다. let coffee = "Americano"; // coffee 값을 대문자로 바꿔 반환하는 함수이다. let getCoffee = function () { return coffee.toUpperCase() }; // coffee 값을 새 값으로 바꾸는 함수이다. let setCoffee = function (newValue) { coffee = newValue }; // coffee, getCoffee, setCoffee를 외부 파일에서 사용할 수 있게 내보낸다. export { coffee, getCoffee, setCoffee };
submodule1.js는coffee,getCoffee,setCoffee를 만든다.
그리고 마지막 줄에서 세 이름을 외부 파일에서 사용할 수 있게export한다.
coffee는 처음에"Americano"값을 가진다.
getCoffee()는 현재coffee값을 대문자로 바꾸어 반환한다.
setCoffee(newValue)는coffee값을 새로운 값으로 바꾼다.
이번 예제에서는mainmodule3.js가 이 세 값을 모두myCoffee라는 묶음으로 가져와 사용한다.
submodule1.js가 내보낸 여러 이름은import * as myCoffee를 통해 하나의 묶음으로 접근할 수 있다.
이 예제에서 확인할 핵심
exam20.html은mainmodule3.js를type="module"로 실행한다.submodule1.js는coffee,getCoffee,setCoffee를export한다.mainmodule3.js는import * as myCoffee로 내보낸 값들을 한 번에 가져온다.myCoffee.coffee는coffee값을 가리킨다.myCoffee.getCoffee()는getCoffee함수를 실행한다.myCoffee.setCoffee("Latte")는coffee값을"Latte"로 바꾼다.getCoffee()는 현재coffee값을 대문자로 바꾸어 반환한다.이 예제는 여러
named export를 하나의 모듈 묶음으로 가져와 사용하는 방법을 보여 준다.
import * as 문법 이해하기
// mainmodule3.js // submodule1.js에서 export한 값들을 myCoffee라는 이름으로 묶어서 가져온다. import * as myCoffee from "./submodule1.js";
import * as myCoffee는submodule1.js가 내보낸 값들을 모두 가져와myCoffee라는 이름으로 묶는 문법이다.
여기서*는 내보낸 값 전체를 의미한다.
myCoffee는 현재 파일에서 사용할 묶음 이름이다.
이제submodule1.js에서 내보낸 이름들은myCoffee안에 들어 있는 것처럼 접근한다.
아래처럼 사용한다.
myCoffee.coffeemyCoffee.getCoffee()myCoffee.setCoffee("Latte")
import * as 이름을 사용하면 내보낸 값들을 하나의 이름 아래에 모아서 사용할 수 있다.
myCoffee.coffee 출력하기
// mainmodule3.js // myCoffee 안의 coffee 값을 출력한다. console.log(myCoffee.coffee);
myCoffee.coffee는submodule1.js에서 내보낸coffee값을 가리킨다.
submodule1.js에서coffee의 처음 값은"Americano"이다.
따라서 첫 번째 콘솔 출력 결과는 아래와 같다.// 콘솔 출력 결과 // Americano이때
coffee값을 바로console.log(coffee)처럼 쓰는 것이 아니다.
모듈 전체 가져오기를 했기 때문에myCoffee.coffee처럼 접근해야 한다.
모듈 전체 가져오기에서는 내보낸 값에 접근할 때 묶음 이름을 반드시 붙인다.
myCoffee.getCoffee() 실행하기
// mainmodule3.js // myCoffee 안의 getCoffee 함수를 실행하고 결과를 출력한다. console.log(myCoffee.getCoffee());
myCoffee.getCoffee()는submodule1.js에서 내보낸getCoffee함수를 실행한다.
getCoffee함수는 현재coffee값을 대문자로 바꾸어 반환한다.
처음coffee값은"Americano"이다.
"Americano".toUpperCase()의 결과는"AMERICANO"이다.
따라서 두 번째 콘솔 출력 결과는 아래와 같다.// 콘솔 출력 결과 // AMERICANO모듈 묶음 안의 함수도
묶음이름.함수이름()형태로 호출한다.
setCoffee로 모듈 내부 값 변경하기
// mainmodule3.js // myCoffee 안의 setCoffee 함수를 실행해 coffee 값을 Latte로 바꾼다. myCoffee.setCoffee("Latte"); console.log(myCoffee.getCoffee());
myCoffee.setCoffee("Latte")는submodule1.js에서 내보낸setCoffee함수를 실행한다.
이 함수는 전달받은 값을coffee변수에 다시 저장한다.
처음coffee값은"Americano"였다.
하지만setCoffee("Latte")가 실행되면coffee값은"Latte"로 바뀐다.
그다음myCoffee.getCoffee()를 다시 실행한다.
이제getCoffee()는 바뀐 값"Latte"를 기준으로toUpperCase()를 실행한다.
그래서 결과는"LATTE"가 된다.
콘솔 출력 결과는 아래와 같다.// 콘솔 출력 결과 // LATTE모듈 안의 값이 함수로 변경되면, 이후 같은 모듈에서 그 값을 사용하는 함수도 변경된 값을 기준으로 동작한다.
전체 콘솔 출력 결과
exam20.html을 실행하면mainmodule3.js가module로 실행된다.
그리고myCoffee묶음을 통해coffee,getCoffee,setCoffee를 사용한다.
전체 콘솔 출력 결과는 아래와 같다.// 콘솔 출력 결과 // Americano // AMERICANO // LATTE첫 번째 줄은
myCoffee.coffee의 결과이다.
처음coffee값인"Americano"가 출력된다.
두 번째 줄은myCoffee.getCoffee()의 결과이다.
처음coffee값"Americano"를 대문자로 바꾼"AMERICANO"가 출력된다.
세 번째 줄은setCoffee("Latte")실행 후 다시myCoffee.getCoffee()를 호출한 결과이다.
변경된 값"Latte"를 대문자로 바꾼"LATTE"가 출력된다.
출력 결과를 보면setCoffee()로 모듈 내부의coffee값이 바뀐 뒤,getCoffee()의 결과도 함께 바뀌는 것을 확인할 수 있다.
모듈 전체 가져오기와 선택 가져오기 비교하기
모듈에서 값을 가져오는 방식은 여러 가지가 있다.
필요한 값만 직접 가져올 수도 있고, 전체를 묶어서 가져올 수도 있다.
필요한 값만 직접 가져오면 아래처럼 쓴다.// mainmodule1.js // 필요한 값만 직접 가져오는 방식이다. import { coffee, getCoffee } from "./submodule1.js"; console.log(coffee); console.log(getCoffee());이 방식에서는
coffee,getCoffee()를 바로 사용할 수 있다.
전체를 묶어서 가져오면 아래처럼 쓴다.// mainmodule3.js // 내보낸 값들을 myCoffee라는 이름으로 묶어서 가져오는 방식이다. import * as myCoffee from "./submodule1.js"; console.log(myCoffee.coffee); console.log(myCoffee.getCoffee());이 방식에서는
myCoffee.를 붙여야 한다.
정리하면 아래처럼 구분할 수 있다.
import { coffee }는 필요한 값을 직접 가져온다.import * as myCoffee는 내보낸 값들을 하나의 묶음으로 가져온다.- 직접 가져오면
coffee처럼 바로 사용한다.- 묶어서 가져오면
myCoffee.coffee처럼 사용한다.필요한 값만 간단히 쓸 때는 선택 가져오기를 사용하고, 모듈의 여러 값을 하나의 묶음으로 관리하고 싶을 때는 전체 가져오기를 사용할 수 있다.
핵심 정리
이 예제의 핵심은
import * as를 사용해서 모듈의 여러 내보내기 값을 하나로 묶어 가져오는 방법이다.
submodule1.js는 여러 값을export하고,mainmodule3.js는 그 값들을myCoffee라는 이름으로 묶어서 사용한다.
이 흐름은 아래처럼 정리할 수 있다.
import * as myCoffee from "./submodule1.js"는submodule1.js가 내보낸 값들을myCoffee라는 이름으로 묶어 가져온다.myCoffee.coffee는submodule1.js의coffee값을 가리킨다.myCoffee.getCoffee()는submodule1.js의getCoffee함수를 실행한다.myCoffee.setCoffee("Latte")는coffee값을"Latte"로 변경한다.- 변경 후
myCoffee.getCoffee()를 다시 실행하면"LATTE"가 반환된다.- 전체 가져오기를 사용하면 가져온 값 앞에 반드시 묶음 이름을 붙여야 한다.
import를 사용하는 파일은type="module"로 실행해야 한다.따라서 이 예제가 알려 주는 핵심은 이것이다.
import * as는 모듈에서 내보낸 여러 값을 하나의 묶음 이름으로 가져오고, 그 안의 값과 함수는묶음이름.내보낸이름형태로 사용하는 문법이다.
이 예제는
export default로 내보낸 객체를 다른 파일에서 가져와 사용하는 흐름을 보여 준다.
앞 예제에서는named export로 여러 값을 이름별로 내보냈고, 가져올 때 중괄호{}를 사용했다.
이번 예제는 다르다.
submodule2.js는coffee객체 하나를 기본값으로 내보낸다.
mainmodule4.js는 그 기본값을unico라는 이름으로 가져온다.
default export의 핵심은 한 파일의 대표 값을 하나 정해서 내보내고, 가져오는 쪽에서 원하는 이름으로 받을 수 있다는 점이다.
이 예제에서는coffee객체 안에getName()과setName()메서드가 들어 있다.
getName()은 현재 커피 이름을 반환하고,setName()은 커피 이름을 새 값으로 바꾼다.
default export란 무엇인가
default export는 한 파일에서 대표로 내보낼 값을 지정하는 문법이다.
export default 값형태로 작성한다.
앞에서 본named export는 여러 값을 이름별로 내보냈다.
그래서 가져올 때도import { coffee, getCoffee }처럼 중괄호 안에 정확한 이름을 적어야 했다.
반면default export는 파일의 대표 값을 하나 내보내는 방식이다.
가져오는 쪽에서는 중괄호를 쓰지 않는다.
그리고 가져오는 파일에서 원하는 이름을 붙여 사용할 수 있다.
정리하면 아래처럼 볼 수 있다.
named export는 여러 값을 이름으로 내보낸다.named import는 중괄호{}안에 내보낸 이름을 적어 가져온다.default export는 파일의 대표 값을 하나 내보낸다.default import는 중괄호 없이 원하는 이름으로 가져온다.
default export로 내보낸 값은 가져올 때 이름을 자유롭게 정할 수 있다.
예제 전체 코드 1
// exam21.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>ECMAScript6</title> </head> <body> <!-- mainmodule4.js 안에서 import를 사용하므로 type="module"이 필요하다. --> <script src="./mainmodule4.js" type="module"></script> </body> </html>
exam21.html은mainmodule4.js를 실행하는HTML파일이다.
여기서도type="module"이 필요하다.
mainmodule4.js안에는import문법이 들어 있다.
그래서 브라우저가 이 파일을 일반script가 아니라module로 실행해야 한다.
import를 사용하는JavaScript파일은HTML에서type="module"로 불러와야 한다.
예제 전체 코드 2
// mainmodule4.js // submodule2.js에서 default export된 값을 unico라는 이름으로 가져온다. import unico from "./submodule2.js"; // 가져온 객체의 getName 메서드를 실행해 현재 이름을 출력한다. console.log(unico.getName()); // 가져온 객체의 setName 메서드를 실행해 이름을 Cappuccino로 바꾼다. unico.setName("Cappuccino"); // 변경된 이름을 다시 확인한다. console.log(unico.getName());
mainmodule4.js는submodule2.js에서 기본으로 내보낸 값을 가져온다.
가져올 때import unico from "./submodule2.js";라고 작성한다.
여기서unico는 가져오는 쪽에서 붙인 이름이다.
submodule2.js에서 실제로 내보낸 객체 이름은coffee이지만,mainmodule4.js에서는 그 객체를unico라는 이름으로 사용한다.
가져온unico객체에는getName()과setName()이 들어 있다.
그래서unico.getName()으로 현재 이름을 확인하고,unico.setName("Cappuccino")로 이름을 바꿀 수 있다.
default export로 가져온 값은 현재 파일에서 붙인 이름으로 사용한다.
예제 전체 코드 3
// submodule2.js // 커피 이름을 저장하는 변수이다. let name = "Americano"; // 커피 이름을 읽고 바꾸는 메서드를 가진 객체이다. let coffee = { // 현재 name 값을 반환한다. getName: function () { return name }, // 전달받은 새 이름으로 name 값을 변경한다. setName: function (newName) { name = newName } }; // coffee 객체를 이 파일의 기본값으로 내보낸다. export default coffee;
submodule2.js는name변수와coffee객체를 가진다.
name변수에는 처음에"Americano"가 저장되어 있다.
coffee객체에는 메서드 두 개가 들어 있다.
getName()은 현재name값을 반환한다.
setName(newName)은name값을 새 값으로 바꾼다.
마지막의export default coffee;는coffee객체를 이 파일의 기본 내보내기 값으로 지정한다.
그래서 다른 파일에서 이 값을 가져올 때 중괄호 없이 가져올 수 있다.
export default coffee는coffee객체를 이 파일의 대표 내보내기 값으로 지정하는 코드이다.
이 예제에서 확인할 핵심
exam21.html은mainmodule4.js를type="module"로 실행한다.submodule2.js는coffee객체를export default로 내보낸다.mainmodule4.js는 기본 내보내기 값을unico라는 이름으로 가져온다.default import는 중괄호{}를 사용하지 않는다.unico.getName()은 현재name값을 반환한다.- 처음
name값은"Americano"이다.unico.setName("Cappuccino")은name값을"Cappuccino"로 바꾼다.- 다시
unico.getName()을 실행하면"Cappuccino"가 반환된다.이 예제는
default export로 내보낸 객체를 원하는 이름으로 가져와 사용하는 방법을 보여 준다.
exam21.html에서 module로 실행하기
// exam21.html <!-- mainmodule4.js 안에서 import를 사용하므로 type="module"이 필요하다. --> <script src="./mainmodule4.js" type="module"></script>
exam21.html은mainmodule4.js를 불러온다.
이때type="module"을 붙인다.
그 이유는mainmodule4.js안에import문법이 있기 때문이다.
import문법은module방식으로 실행될 때 사용한다.
그래서HTML에서module파일을 실행하려면script태그에type="module"을 붙여야 한다.
type="module"은import/export문법을 브라우저에서 실행하기 위한 설정이다.
coffee 객체를 default로 내보내기
// submodule2.js // 커피 이름을 저장하는 변수이다. let name = "Americano"; // 커피 이름을 읽고 바꾸는 메서드를 가진 객체이다. let coffee = { getName: function () { return name }, setName: function (newName) { name = newName } }; // coffee 객체를 이 파일의 기본값으로 내보낸다. export default coffee;
submodule2.js에서 실제로 내보내는 값은coffee객체이다.
이 객체는getName()과setName()을 가진다.
export default coffee;는 이 파일에서coffee객체를 기본값으로 내보낸다는 뜻이다.
기본값으로 내보냈기 때문에 가져오는 파일에서는 중괄호 없이 가져온다.
name변수 자체는 직접export하지 않았다.
하지만coffee객체의 메서드들이name변수를 사용한다.
그래서 외부 파일은name을 직접 만지지 않고,getName()과setName()을 통해 값을 확인하거나 바꾼다.
name은 직접 내보내지 않았지만,coffee객체의 메서드를 통해 간접적으로 읽고 바꿀 수 있다.
default import로 원하는 이름 붙이기
// mainmodule4.js // submodule2.js에서 default export된 값을 unico라는 이름으로 가져온다. import unico from "./submodule2.js";이 코드는
submodule2.js에서default export한 값을 가져온다.
가져온 값에 현재 파일에서 사용할 이름으로unico를 붙인다.
여기서 중요한 점은submodule2.js에서 객체 이름이coffee였다는 것이다.
하지만mainmodule4.js에서는 그 이름을 그대로 쓸 필요가 없다.
default export는 가져오는 쪽에서 이름을 정할 수 있기 때문이다.
그래서 아래처럼 이해하면 된다.
submodule2.js에서는coffee객체를 기본값으로 내보낸다.mainmodule4.js에서는 그 기본값을unico라는 이름으로 받는다.- 이후 현재 파일에서는
unico라는 이름으로 객체를 사용한다.
default import에서는 내보낸 쪽의 변수명보다 가져오는 쪽에서 붙인 이름이 현재 파일의 사용 이름이 된다.
getName으로 현재 이름 확인하기
// mainmodule4.js // 가져온 객체의 getName 메서드를 실행해 현재 이름을 출력한다. console.log(unico.getName());
unico는submodule2.js에서 가져온coffee객체이다.
이 객체 안에는getName()메서드가 있다.
getName()은submodule2.js안의name값을 반환한다.
처음name값은"Americano"이다.
따라서 첫 번째 콘솔 출력 결과는 아래와 같다.// 콘솔 출력 결과 // Americano
unico.getName()은 가져온 객체 안의 메서드를 실행해서 현재 커피 이름을 확인하는 코드이다.
setName으로 이름 변경하기
// mainmodule4.js // 가져온 객체의 setName 메서드를 실행해 이름을 Cappuccino로 바꾼다. unico.setName("Cappuccino");
unico.setName("Cappuccino")은 가져온 객체의setName()메서드를 실행한다.
setName(newName)은 전달받은 값을name변수에 저장한다.
여기서는"Cappuccino"를 전달했다.
그래서submodule2.js안의name값이"Americano"에서"Cappuccino"로 바뀐다.
외부 파일이name변수에 직접 접근한 것이 아니라, 객체의 메서드를 통해 값을 바꾼 것이다.
변경된 이름 다시 확인하기
// mainmodule4.js // 변경된 이름을 다시 확인한다. console.log(unico.getName());이 코드는 이름이 변경된 뒤 다시
getName()을 실행한다.
앞에서setName("Cappuccino")이 실행되었기 때문에name값은 이제"Cappuccino"이다.
따라서 두 번째 콘솔 출력 결과는 아래와 같다.// 콘솔 출력 결과 // Cappuccino같은 객체의 메서드가 같은 내부 변수
name을 사용하므로,setName()으로 바꾼 값은 이후getName()결과에도 반영된다.
named export와 default export 비교하기
앞 예제의
submodule1.js는named export를 사용했다.// submodule1.js // 여러 값을 이름별로 내보낸다. export { coffee, getCoffee, setCoffee };이 경우 가져올 때는 중괄호 안에 내보낸 이름을 적어야 한다.
// mainmodule1.js // named export는 중괄호 안에 이름을 적어 가져온다. import { coffee, getCoffee } from "./submodule1.js";반면 이번 예제의
submodule2.js는default export를 사용한다.// submodule2.js // coffee 객체를 기본값으로 내보낸다. export default coffee;이 경우 가져올 때 중괄호를 쓰지 않는다.
그리고 가져오는 쪽에서 원하는 이름을 붙인다.// mainmodule4.js // default export는 중괄호 없이 원하는 이름으로 가져온다. import unico from "./submodule2.js";정리하면 아래처럼 볼 수 있다.
named export는 여러 값을 이름별로 내보낼 때 사용한다.named import는 중괄호{}를 사용한다.default export는 파일의 대표 값을 하나 내보낼 때 사용한다.default import는 중괄호를 사용하지 않는다.default import는 가져오는 쪽에서 이름을 정할 수 있다.
named export는 이름을 맞춰 가져오고,default export는 기본값을 원하는 이름으로 가져온다.
실행 결과 정리
exam21.html을 실행하면mainmodule4.js가module로 실행된다.
mainmodule4.js는submodule2.js에서 기본으로 내보낸coffee객체를unico라는 이름으로 가져온다.
처음에는unico.getName()을 실행한다.
이때name값은"Americano"이다.
그다음unico.setName("Cappuccino")을 실행한다.
이 코드가name값을"Cappuccino"로 바꾼다.
마지막으로 다시unico.getName()을 실행한다.
이제 변경된 값"Cappuccino"가 반환된다.
전체 콘솔 출력 결과는 아래와 같다.// 콘솔 출력 결과 // Americano // Cappuccino출력 결과를 보면
default export로 가져온 객체의 메서드를 통해 내부 값이 변경되는 흐름을 확인할 수 있다.
핵심 정리
이 예제의 핵심은
default export로 내보낸 값을 중괄호 없이 가져와 사용하는 방법이다.
submodule2.js는coffee객체를 기본값으로 내보내고,mainmodule4.js는 그 값을unico라는 이름으로 가져온다.
이 흐름은 아래처럼 정리할 수 있다.
export default coffee는coffee객체를 기본값으로 내보낸다.import unico from "./submodule2.js"는 기본값을unico라는 이름으로 가져온다.default import는 중괄호{}를 사용하지 않는다.default import에서는 가져오는 쪽에서 이름을 정할 수 있다.unico는 실제로submodule2.js의coffee객체를 가리킨다.unico.getName()은 현재name값을 반환한다.- 처음
name값은"Americano"이다.unico.setName("Cappuccino")은name값을"Cappuccino"로 바꾼다.- 다시
unico.getName()을 실행하면"Cappuccino"가 반환된다.import를 사용하는 파일은type="module"로 실행해야 한다.따라서 이 예제가 알려 주는 핵심은 이것이다.
default export는 한 파일의 대표 값을 내보내는 방식이고, 가져오는 파일에서는 중괄호 없이 원하는 이름으로 받아 사용할 수 있다.
이 예제는 하나의
module파일에서default export와named export를 함께 내보내고, 다른 파일에서 둘을 동시에 가져오는 흐름을 보여 준다.
앞 예제에서는default export로 내보낸 객체 하나만 가져왔다.
이번 예제에서는default export로 내보낸 객체와named export로 내보낸 변수name을 함께 가져온다.
default export는 중괄호 없이 가져온다.
named export는 중괄호{}안에 이름을 적어 가져온다.
이 예제의 핵심은default import와named import를 한 줄에서 함께 작성할 수 있다는 점이다.
mainmodule5.js는submodule3.js에서 기본으로 내보낸coffee객체를unico라는 이름으로 가져온다.
또named export된name변수도 함께 가져온다.
default export와 named export를 함께 쓴다는 뜻
module에서는 한 파일 안에서 기본 내보내기와 이름 있는 내보내기를 함께 사용할 수 있다.
default export는 그 파일의 대표 값을 하나 내보내는 방식이다.
가져오는 쪽에서는 중괄호 없이 원하는 이름으로 받을 수 있다.
named export는 이름이 붙은 값을 내보내는 방식이다.
가져오는 쪽에서는 중괄호{}안에 내보낸 이름을 정확히 적어야 한다.
이번 예제의submodule3.js는 아래 두 가지를 함께 내보낸다.
name은named export로 내보낸다.coffee객체는default export로 내보낸다.그래서
mainmodule5.js에서는 아래처럼 한 줄로 함께 가져온다.// mainmodule5.js // default export는 unico로 받고, named export인 name은 중괄호 안에 받는다. import unico, { name } from "./submodule3.js";
import 기본이름, { 이름있는값 } from "파일경로"형태로default export와named export를 함께 가져올 수 있다.
예제 전체 코드 1
// exam22.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>ECMAScript6</title> </head> <body> <!-- mainmodule5.js 안에서 import를 사용하므로 type="module"이 필요하다. --> <script src="./mainmodule5.js" type="module"></script> </body> </html>
exam22.html은mainmodule5.js를 실행하는HTML파일이다.
여기서 중요한 부분은type="module"이다.
mainmodule5.js안에는import문법이 들어 있다.
따라서 일반script가 아니라module방식으로 실행해야 한다.
import/export를 사용하는 파일은HTML에서type="module"로 불러와야 한다.
예제 전체 코드 2
// mainmodule5.js // default export는 unico라는 이름으로 가져오고, named export인 name은 중괄호로 가져온다. import unico, { name } from "./submodule3.js"; // named export로 가져온 name 값을 출력한다. console.log(name); // default export로 가져온 객체의 getName 메서드를 실행한다. console.log(unico.getName()); // default export로 가져온 객체의 setName 메서드를 실행해 name 값을 Espresso로 바꾼다. unico.setName("Espresso"); // 변경된 name 값을 getName 메서드로 다시 확인한다. console.log(unico.getName());
mainmodule5.js는submodule3.js에서 두 가지를 가져온다.
하나는default export로 내보낸 객체이다.
이 객체는unico라는 이름으로 가져온다.
다른 하나는named export로 내보낸name변수이다.
named export는 이름이 정해져 있으므로{ name }처럼 중괄호 안에 적어 가져온다.
이후console.log(name)으로name값을 출력한다.
그리고unico.getName()으로 같은 이름 값을 객체 메서드를 통해 확인한다.
마지막으로unico.setName("Espresso")를 실행해서 이름을 바꾼 뒤, 다시unico.getName()으로 변경 결과를 확인한다.
unico는 기본으로 가져온 객체이고,name은 이름으로 가져온 변수이다.
예제 전체 코드 3
// submodule3.js // name 변수를 named export로 내보낸다. export let name = "Americano"; // name 값을 읽고 바꾸는 메서드를 가진 객체이다. let coffee = { // 현재 name 값을 반환한다. getName: function () { return name }, // 전달받은 새 이름으로 name 값을 변경한다. setName: function (newName) { name = newName } }; // coffee 객체를 default export로 내보낸다. export default coffee
submodule3.js는name변수를 직접export한다.
그래서 다른 파일에서{ name }형태로 가져올 수 있다.
또coffee객체를 만든다.
이 객체 안에는getName()과setName()이 있다.
getName()은 현재name값을 반환한다.
setName(newName)은name값을 새 값으로 바꾼다.
마지막 줄의export default coffee는coffee객체를 이 파일의 기본 내보내기 값으로 지정한다.
그래서 다른 파일에서 중괄호 없이 원하는 이름으로 가져올 수 있다.
submodule3.js는name은 이름으로 내보내고,coffee객체는 기본값으로 내보낸다.
이 예제에서 확인할 핵심
exam22.html은mainmodule5.js를type="module"로 실행한다.submodule3.js는name을named export로 내보낸다.submodule3.js는coffee객체를default export로 내보낸다.mainmodule5.js는 기본 내보내기 값을unico라는 이름으로 가져온다.mainmodule5.js는 이름 있는 내보내기 값name도 함께 가져온다.default import는 중괄호 없이 작성한다.named import는 중괄호{}안에 작성한다.unico.getName()은 현재name값을 반환한다.unico.setName("Espresso")는name값을"Espresso"로 변경한다.이 예제는 한 파일에서 기본 내보내기와 이름 있는 내보내기를 함께 사용하고, 가져오는 파일에서도 둘을 동시에 받는 방법을 보여 준다.
exam22.html에서 module로 실행하기
// exam22.html <!-- mainmodule5.js 안에서 import를 사용하므로 type="module"이 필요하다. --> <script src="./mainmodule5.js" type="module"></script>
exam22.html은mainmodule5.js를 불러온다.
이때type="module"을 붙인다.
그 이유는mainmodule5.js안에import문법이 있기 때문이다.
import문법은module방식에서 동작한다.
그래서HTML에서mainmodule5.js를 실행하려면script태그에type="module"을 지정해야 한다.
type="module"은 브라우저가 해당script를module문법으로 해석하게 만드는 설정이다.
submodule3.js에서 name을 named export로 내보내기
// submodule3.js // name 변수를 named export로 내보낸다. export let name = "Americano";
export let name = "Americano";는name변수를 만들면서 동시에 외부로 내보내는 코드이다.
이 방식은named export이다.
named export는 이름이 중요하다.
여기서는name이라는 이름으로 내보냈기 때문에, 가져오는 쪽에서도{ name }처럼 이름을 맞춰 가져온다.
처음name값은"Americano"이다.
그래서mainmodule5.js에서console.log(name)을 실행하면"Americano"가 출력된다.
// 콘솔 출력 결과 // Americano
named export로 내보낸 값은 가져오는 쪽에서 중괄호 안에 정확한 이름을 적어야 한다.
coffee 객체를 default export로 내보내기
// submodule3.js // name 값을 읽고 바꾸는 메서드를 가진 객체이다. let coffee = { getName: function () { return name }, setName: function (newName) { name = newName } }; // coffee 객체를 default export로 내보낸다. export default coffee
coffee객체는getName()과setName()메서드를 가진다.
이 두 메서드는 모두 같은name변수를 사용한다.
getName()은 현재name값을 반환한다.
처음에는name이"Americano"이므로getName()도"Americano"를 반환한다.
setName(newName)은name값을 새 값으로 바꾼다.
예를 들어"Espresso"를 전달하면name값이"Espresso"로 바뀐다.
마지막의export default coffee는coffee객체를 기본 내보내기 값으로 지정한다.
그래서 가져오는 쪽에서는 중괄호 없이 가져올 수 있다.
default export는 파일의 대표 값을 하나 내보내고, 가져오는 쪽에서 원하는 이름으로 받을 수 있게 한다.
default import와 named import를 한 줄에 작성하기
// mainmodule5.js // default export는 unico라는 이름으로 가져오고, named export인 name은 중괄호로 가져온다. import unico, { name } from "./submodule3.js";이 코드는
default import와named import를 한 줄에 같이 작성한 코드이다.
unico는default export로 내보낸coffee객체를 받는 이름이다.
default import는 중괄호를 사용하지 않는다.
{ name }은named export로 내보낸name변수를 받는 부분이다.
named import는 중괄호를 사용한다.
정리하면 아래처럼 볼 수 있다.
unico는default export된coffee객체를 가리킨다.{ name }은named export된name변수를 가져온다.default import는 중괄호 밖에 쓴다.named import는 중괄호 안에 쓴다.한 파일에서
default export와named export를 함께 내보냈다면, 가져올 때도기본이름, { 이름있는값 }형태로 함께 받을 수 있다.
name 값을 직접 출력하기
// mainmodule5.js // named export로 가져온 name 값을 출력한다. console.log(name);
name은submodule3.js에서named export로 가져온 값이다.
처음 값은"Americano"이다.
따라서 첫 번째 콘솔 출력 결과는 아래와 같다.// 콘솔 출력 결과 // Americano이 출력은
unico객체의 메서드를 사용한 것이 아니다.
named import로 직접 가져온name값을 출력한 것이다.
같은 파일에서 가져왔더라도,name과unico는 사용하는 방식이 다르다.
unico.getName으로 name 값 확인하기
// mainmodule5.js // default export로 가져온 객체의 getName 메서드를 실행한다. console.log(unico.getName());
unico는default export로 가져온coffee객체이다.
이 객체 안에는getName()메서드가 있다.
getName()은submodule3.js안의name값을 반환한다.
현재name은 아직"Americano"이다.
따라서 두 번째 콘솔 출력 결과도 아래와 같다.// 콘솔 출력 결과 // Americano첫 번째 출력은
name변수를 직접 출력한 결과이다.
두 번째 출력은unico.getName()을 통해name값을 확인한 결과이다.
name을 직접 가져와 출력해도 되고, 기본으로 가져온 객체의 메서드를 통해 확인할 수도 있다.
unico.setName으로 name 값 변경하기
// mainmodule5.js // default export로 가져온 객체의 setName 메서드를 실행해 name 값을 Espresso로 바꾼다. unico.setName("Espresso"); console.log(unico.getName());
unico.setName("Espresso")는coffee객체의setName()메서드를 실행한다.
이 메서드는 전달받은"Espresso"를name변수에 저장한다.
즉,submodule3.js안의name값이"Americano"에서"Espresso"로 바뀐다.
그다음unico.getName()을 다시 실행하면 변경된 값이 반환된다.
따라서 마지막 콘솔 출력 결과는 아래와 같다.// 콘솔 출력 결과 // Espresso여기서 중요한 점은
mainmodule5.js가name을 직접 수정한 것이 아니라는 점이다.
setName()메서드를 통해submodule3.js안의name값을 바꾼 것이다.
내보낸 변수 값을 직접 바꾸는 것이 아니라, 함께 내보낸 객체의 메서드를 통해 값을 변경하는 흐름이다.
전체 콘솔 출력 결과
exam22.html을 실행하면mainmodule5.js가module로 실행된다.
이후submodule3.js에서 가져온name과unico를 사용한다.
전체 콘솔 출력 결과는 아래와 같다.// 콘솔 출력 결과 // Americano // Americano // Espresso첫 번째 줄은
console.log(name)의 결과이다.
named import로 가져온name값이 출력된다.
두 번째 줄은console.log(unico.getName())의 결과이다.
unico객체의getName()메서드가 현재name값을 반환한다.
세 번째 줄은unico.setName("Espresso")실행 후 다시unico.getName()을 호출한 결과이다.
name값이"Espresso"로 바뀌었기 때문에"Espresso"가 출력된다.
출력 결과를 보면named export된 값과default export된 객체가 같은 내부 값name을 기준으로 연결되어 있음을 확인할 수 있다.
이전 default export 예제와 비교하기
앞 예제의
submodule2.js는coffee객체만default export로 내보냈다.
그래서mainmodule4.js에서는 아래처럼 가져왔다.// mainmodule4.js // default export 하나만 가져오는 방식이다. import unico from "./submodule2.js";이 경우에는 중괄호가 필요 없었다.
기본값 하나만 가져오기 때문이다.
이번 예제의submodule3.js는 다르다.
coffee객체는default export로 내보내고,name은named export로 내보낸다.
그래서mainmodule5.js에서는 아래처럼 가져온다.// mainmodule5.js // default export와 named export를 함께 가져오는 방식이다. import unico, { name } from "./submodule3.js";정리하면 아래처럼 볼 수 있다.
default export만 있으면 중괄호 없이 하나만 가져온다.default export와named export가 함께 있으면 기본값은 중괄호 밖에 쓴다.- 이름 있는 값은 중괄호 안에 쓴다.
default import와named import를 함께 쓸 때는 위치가 중요하다. 기본값은 앞에, 이름 있는 값은 중괄호 안에 작성한다.
핵심 정리
이 예제의 핵심은 한 파일에서
default export와named export를 함께 내보내고, 가져오는 파일에서 둘을 동시에 사용하는 방법이다.
submodule3.js는name을 이름으로 내보내고,coffee객체를 기본값으로 내보낸다.
이 흐름은 아래처럼 정리할 수 있다.
export let name = "Americano"는name을named export로 내보낸다.export default coffee는coffee객체를 기본값으로 내보낸다.import unico, { name } from "./submodule3.js"는 기본값과 이름 있는 값을 함께 가져온다.unico는default export로 가져온coffee객체이다.name은named export로 가져온 변수이다.console.log(name)은 처음 값"Americano"를 출력한다.unico.getName()도 처음에는"Americano"를 반환한다.unico.setName("Espresso")는name값을"Espresso"로 바꾼다.- 다시
unico.getName()을 실행하면"Espresso"가 반환된다.default import는 중괄호 밖에 쓰고,named import는 중괄호 안에 쓴다.따라서 이 예제가 알려 주는 핵심은 이것이다.
한 파일에서default export와named export를 함께 사용할 수 있고, 가져올 때는 기본값은 중괄호 없이, 이름 있는 값은 중괄호 안에 작성한다.
이 예제는
module에서 가져온 값을 브라우저 화면에 출력하는 흐름을 보여 준다.
앞 예제에서는console.log()로 가져온 값을 콘솔에서 확인했다.
이번 예제에서는HTML문서 안의div요소를 찾아서 그 안에 결과를 직접 넣는다.
mathUtils.js는 계산에 필요한 값과 함수를 내보낸다.
main.js는mathUtils.js에서default export와named export를 함께 가져온다.
그리고 가져온 값으로 화면에 제목, 원주율, 덧셈 결과를 출력한다.
이 예제의 핵심은module에서 가져온 값을 실제HTML화면 출력에 연결하는 흐름을 이해하는 것이다.
이번 예제에서 먼저 알아야 할 흐름
이 예제는 파일이 세 개로 나뉜다.
각 파일의 역할이 다르다.
exam23.html은 브라우저에서 실행되는 시작 파일이다.
이 파일은 화면에 결과가 들어갈 자리인div를 만들고,main.js를module로 불러온다.
mathUtils.js는 값을 내보내는 파일이다.
원주율 값인PI, 덧셈 함수인add(), 계산기 이름인calculatorName을 준비한다.
그중PI와add()는named export로 내보내고,calculatorName은default export로 내보낸다.
main.js는 가져와서 사용하는 파일이다.
mathUtils.js에서 내보낸 값들을 가져온 뒤,document.getElementById()로 화면 요소를 찾고,innerHTML로 내용을 넣는다.
이번 예제는 내보내는 파일, 가져오는 파일, 화면에 출력하는HTML파일이 서로 연결되는 구조이다.
DOM이란 무엇인가
DOM은 브라우저가HTML문서를 코드로 다룰 수 있게 만든 구조이다.
쉽게 말하면HTML태그를JavaScript에서 찾고 바꿀 수 있게 만든 문서 객체 구조이다.
예를 들어exam23.html에는 아래 요소가 있다.// exam23.html <div id="app"></div>이
div는 처음에는 비어 있다.
하지만main.js에서 이 요소를 찾아 내용을 넣으면, 브라우저 화면에 결과가 표시된다.
이때 사용하는 코드가document.getElementById('app')이다.
document는 현재HTML문서 전체를 의미한다.
getElementById('app')는id가app인 요소를 찾는 메서드이다.
DOM을 사용하면JavaScript로HTML요소를 찾고, 그 안의 내용을 바꿀 수 있다.
예제 전체 코드 1
// exam23.html <!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8"> <title>ESM 테스트</title> </head> <body> <!-- main.js에서 결과를 넣을 화면 영역이다. --> <div id="app"></div> <!-- type="module"을 지정해야 import/export가 작동한다. --> <script type="module" src="./src/main.js"></script> </body> </html>
exam23.html은 화면의 시작점이다.
<div id="app"></div>는 나중에JavaScript가 결과를 넣을 자리이다.
아래쪽의script태그는main.js를 불러온다.
여기서 중요한 부분은type="module"이다.
main.js안에서import문법을 사용하기 때문에 반드시module방식으로 실행해야 한다.
import/export를 사용하는JavaScript파일은script type="module"로 불러와야 한다.
예제 전체 코드 2
// src/mathUtils.js // named export이다. 원주율 값을 다른 파일에서 이름으로 가져갈 수 있게 한다. export const PI = 3.141592; // named export이다. 두 숫자를 더하는 함수를 내보낸다. export function add(a, b) { // 전달받은 두 값을 더해서 반환한다. return a + b; } // default export로 내보낼 계산기 이름을 변수에 저장한다. const calculatorName = "My Simple Calculator"; // 이 파일의 기본 내보내기 값으로 calculatorName을 내보낸다. export default calculatorName;
mathUtils.js는 값을 내보내는 파일이다.
여기서는 세 가지를 준비한다.
첫 번째는PI이다.
PI는 원주율 값을 가진 상수이다.
export const PI = 3.141592;처럼 작성했기 때문에named export이다.
두 번째는add()함수이다.
add(a, b)는 숫자 두 개를 받아 더한 결과를 반환한다.
이 함수도export function add(...)형태이므로named export이다.
세 번째는calculatorName이다.
이 값은export default calculatorName;으로 내보낸다.
따라서 이 파일의 대표 기본값이 된다.
mathUtils.js는PI와add()는 이름으로 내보내고,calculatorName은 기본값으로 내보낸다.
예제 전체 코드 3
// src/main.js // default export는 중괄호 없이 calcName이라는 이름으로 가져온다. import calcName from './mathUtils.js'; // named export인 PI와 add는 중괄호 안에 가져온다. import { PI, add } from './mathUtils.js'; // add 함수를 실행해서 10 + 20 결과를 구한다. const result = add(10, 20); // HTML 문서에서 id가 app인 요소를 찾는다. const app = document.getElementById('app'); // 찾은 app 요소 안에 HTML 내용을 넣는다. app.innerHTML = ` <h1>${calcName}</h1> <p>원주율: ${PI}</p> <p>10 + 20 = ${result}</p> `;
main.js는 가져온 값을 실제로 사용하는 파일이다.
첫 번째import는default export를 가져온다.
mathUtils.js에서 기본값으로 내보낸calculatorName을 현재 파일에서는calcName이라는 이름으로 받는다.
두 번째import는named export를 가져온다.
PI와add는 이름이 있는 내보내기이므로 중괄호{}안에 작성한다.
그다음add(10, 20)을 실행해서 결과를result에 저장한다.
add()함수는 두 값을 더하므로result에는30이 들어간다.
마지막으로document.getElementById('app')으로HTML의div를 찾는다.
그리고app.innerHTML에 템플릿 문자열을 넣어 화면 내용을 만든다.
main.js는 모듈에서 가져온 값과 함수를 사용해HTML화면에 표시할 내용을 만든다.
이 예제에서 확인할 핵심
exam23.html은main.js를type="module"로 불러온다.mathUtils.js는 값을 내보내는 모듈 파일이다.PI는named export로 내보낸다.add()도named export로 내보낸다.calculatorName은default export로 내보낸다.main.js는default export를calcName이라는 이름으로 가져온다.main.js는named export인PI,add를 중괄호로 가져온다.add(10, 20)의 결과는30이다.document.getElementById('app')은 화면에서id가app인 요소를 찾는다.innerHTML은 찾은 요소 안에HTML내용을 넣는다.이 예제는 모듈에서 가져온 값을 콘솔이 아니라 실제 화면 출력에 사용하는 예제이다.
exam23.html에서 화면 출력 위치 준비하기
// exam23.html <!-- main.js에서 결과를 넣을 화면 영역이다. --> <div id="app"></div>이 코드는 화면에 결과가 들어갈 자리를 만든다.
div는 영역을 만드는HTML태그이다.
id="app"은 이 요소를 찾기 위한 이름이다.
현재 이div안에는 아무 내용도 없다.
그래서 처음HTML만 보면 화면에는 특별한 결과가 보이지 않는다.
하지만main.js에서document.getElementById('app')으로 이 요소를 찾고,innerHTML로 내용을 넣으면 화면에 결과가 출력된다.
id="app"은JavaScript가 화면의 특정 위치를 찾아 결과를 넣기 위한 기준이다.
script type="module"로 main.js 실행하기
// exam23.html <!-- type="module"을 지정해야 import/export가 작동한다. --> <script type="module" src="./src/main.js"></script>이 코드는
main.js를 실행한다.
여기서type="module"이 중요하다.
main.js안에는import문법이 있다.
import는module문법이다.
그래서HTML에서 이 파일을 실행할 때type="module"을 지정해야 한다.
src="./src/main.js"는 실행할 파일의 위치를 의미한다.
즉, 현재HTML파일 기준으로src폴더 안의main.js를 불러오겠다는 뜻이다.
type="module"은 브라우저가 이script를 모듈 코드로 해석하게 만드는 설정이다.
named export로 PI와 add 내보내기
// src/mathUtils.js // named export이다. 원주율 값을 다른 파일에서 이름으로 가져갈 수 있게 한다. export const PI = 3.141592; // named export이다. 두 숫자를 더하는 함수를 내보낸다. export function add(a, b) { // 전달받은 두 값을 더해서 반환한다. return a + b; }
PI와add()는named export로 내보낸다.
named export는 이름이 있는 내보내기 방식이다.
PI라는 이름으로 내보냈기 때문에 가져오는 쪽에서도{ PI }처럼 이름을 맞춰 가져온다.
add도 마찬가지이다.
add라는 이름으로 내보냈기 때문에{ add }로 가져온다.
add(a, b)에서a와b는 매개변수이다.
매개변수는 함수가 호출될 때 전달받는 값을 담는 자리이다.
add(10, 20)이 실행되면a에는10,b에는20이 들어간다.
그래서return a + b는30을 반환한다.
named export는 내보낸 이름과 가져오는 이름을 맞춰야 한다.
default export로 calculatorName 내보내기
// src/mathUtils.js // default export로 내보낼 계산기 이름을 변수에 저장한다. const calculatorName = "My Simple Calculator"; // 이 파일의 기본 내보내기 값으로 calculatorName을 내보낸다. export default calculatorName;
calculatorName은 계산기 이름을 담은 문자열이다.
값은"My Simple Calculator"이다.
이 값은export default calculatorName;으로 내보낸다.
즉,mathUtils.js의 기본 내보내기 값이다.
default export는 가져오는 쪽에서 이름을 자유롭게 정할 수 있다.
그래서main.js에서는 이 값을calcName이라는 이름으로 가져온다.
default export는 파일의 대표 값을 하나 내보내고, 가져오는 쪽에서 원하는 이름으로 받을 수 있다.
default import와 named import 함께 사용하기
// src/main.js // default export는 중괄호 없이 calcName이라는 이름으로 가져온다. import calcName from './mathUtils.js'; // named export인 PI와 add는 중괄호 안에 가져온다. import { PI, add } from './mathUtils.js';첫 번째 줄은
default export를 가져온다.
mathUtils.js에서 기본값으로 내보낸calculatorName을calcName이라는 이름으로 받는다.
중괄호는 사용하지 않는다.
두 번째 줄은named export를 가져온다.
PI와add는 이름으로 내보낸 값이므로{ PI, add }처럼 중괄호 안에 작성한다.
이 예제에서는default import와named import를 두 줄로 나누어 작성했다.
앞 예제처럼 한 줄에 같이 쓸 수도 있지만, 여기서는 두 줄로 나누어 더 명확하게 보여 준다.
정리하면 아래처럼 볼 수 있다.
calcName은default export로 가져온 값이다.PI와add는named export로 가져온 값이다.default import는 중괄호 없이 작성한다.named import는 중괄호 안에 작성한다.같은 파일에서 기본값과 이름 있는 값을 가져오더라도, 가져오는 문법은 서로 다르다.
add 함수로 계산 결과 만들기
// src/main.js // add 함수를 실행해서 10 + 20 결과를 구한다. const result = add(10, 20);
add는mathUtils.js에서 가져온 함수이다.
add(10, 20)은 숫자10과20을 함수에 전달한다.
mathUtils.js의add(a, b)함수는 두 값을 더해서 반환한다.
그래서add(10, 20)의 결과는30이다.
이 결과는result변수에 저장된다.
이후 화면에10 + 20 = 30형태로 출력된다.
모듈에서 가져온 함수도 현재 파일의 함수처럼 호출해서 결과를 만들 수 있다.
getElementById로 출력 위치 찾기
// src/main.js // HTML 문서에서 id가 app인 요소를 찾는다. const app = document.getElementById('app');
document는 현재 브라우저에 열려 있는HTML문서 전체를 의미한다.
getElementById('app')은 그 문서 안에서id가app인 요소를 찾는다.
exam23.html에는<div id="app"></div>가 있다.
그래서 이 코드는 그div요소를 찾아app변수에 저장한다.
이제app변수를 통해 해당div안의 내용을 바꿀 수 있다.
document.getElementById('app')은 화면에서 결과를 넣을 위치를 찾는 코드이다.
innerHTML로 화면에 내용 넣기
// src/main.js // 찾은 app 요소 안에 HTML 내용을 넣는다. app.innerHTML = ` <h1>${calcName}</h1> <p>원주율: ${PI}</p> <p>10 + 20 = ${result}</p> `;
innerHTML은 선택한 요소 안에HTML내용을 넣거나 바꿀 때 사용하는 프로퍼티이다.
여기서는app요소 안에 제목과 문단을 넣는다.
백틱으로 감싼 문자열은template literal이다.
template literal안에서는${}를 사용해 변수 값을 문자열 안에 넣을 수 있다.
calcName에는"My Simple Calculator"가 들어 있다.
PI에는3.141592가 들어 있다.
result에는30이 들어 있다.
그래서 최종적으로app요소 안에는 아래와 같은 내용이 들어간다.// 화면에 들어가는 HTML 결과 <h1>My Simple Calculator</h1> <p>원주율: 3.141592</p> <p>10 + 20 = 30</p>
innerHTML을 사용하면JavaScript에서 만든 문자열을 실제HTML화면 내용으로 넣을 수 있다.
화면 출력 결과
이 예제를 실행하면 브라우저 화면에는 계산기 이름, 원주율, 덧셈 결과가 출력된다.
화면 출력 결과는 아래처럼 볼 수 있다.// 화면 출력 결과 // My Simple Calculator // 원주율: 3.141592 // 10 + 20 = 30첫 번째 줄은
default export로 가져온calculatorName값이다.
main.js에서는 이 값을calcName이라는 이름으로 사용한다.
두 번째 줄은named export로 가져온PI값이다.
세 번째 줄은named export로 가져온add()함수를 실행한 결과이다.
화면 출력 결과를 보면 모듈에서 가져온 값들이 실제HTML문서에 반영된 것을 확인할 수 있다.
전체 실행 흐름 정리
이 예제의 전체 실행 흐름은 아래처럼 볼 수 있다.
exam23.html이 실행된다.div id="app"이 화면에 준비된다.script type="module"로main.js를 불러온다.main.js가mathUtils.js에서 기본값calculatorName을calcName으로 가져온다.main.js가mathUtils.js에서PI와add를 이름으로 가져온다.add(10, 20)을 실행해30을 만든다.document.getElementById('app')으로 화면 출력 위치를 찾는다.app.innerHTML에HTML문자열을 넣는다.- 브라우저 화면에 계산기 이름, 원주율, 덧셈 결과가 출력된다.
이 예제는 모듈에서 값을 가져오는 단계와 가져온 값을 화면에 반영하는 단계가 이어지는 구조이다.
핵심 정리
이 예제의 핵심은
module에서 가져온 값을 실제 화면 출력에 사용하는 방법이다.
mathUtils.js는 값을 내보내고,main.js는 그 값을 가져와HTML요소 안에 넣는다.
이 흐름은 아래처럼 정리할 수 있다.
exam23.html은main.js를type="module"로 실행한다.mathUtils.js는PI와add()를named export로 내보낸다.mathUtils.js는calculatorName을default export로 내보낸다.main.js는 기본값을calcName이라는 이름으로 가져온다.main.js는PI,add를 중괄호로 가져온다.add(10, 20)의 결과는30이다.document.getElementById('app')은id가app인HTML요소를 찾는다.innerHTML은 찾은 요소 안에HTML내용을 넣는다.- 화면에는
"My Simple Calculator",3.141592,10 + 20 = 30이 출력된다.따라서 이 예제가 알려 주는 핵심은 이것이다.
module에서 가져온 값과 함수는 콘솔 확인에만 쓰는 것이 아니라,DOM을 통해 실제HTML화면 내용으로도 연결할 수 있다.
이 예제는
default export로 내보낸 함수와named export로 내보낸 함수를 함께 가져와 화면에 출력하는 흐름을 보여 준다.
앞 예제에서는 계산기 이름, 원주율, 덧셈 결과를 화면에 출력했다.
이번 예제에서는 인사 문장을 만드는 함수와 현재 시간을 구하는 함수를 가져와HTML화면에 출력한다.
greet.js는 인사 문장을 만드는 함수를default export로 내보낸다.
또 현재 시간을 구하는getCurrentTime함수를named export로 내보낸다.
app.js는 이 두 함수를 가져와서div id="display"안에 결과를 넣는다.
이 예제의 핵심은 함수도export할 수 있고, 가져온 함수를 실행한 결과를DOM을 통해 화면에 넣을 수 있다는 점이다.
이번 예제의 전체 흐름
이 예제는 파일이 세 개로 나뉜다.
각 파일은 역할이 다르다.
exam24.html은 브라우저에서 실행되는 시작 파일이다.
화면에 결과가 들어갈div id="display"를 만들고,app.js를module방식으로 불러온다.
greet.js는 함수를 내보내는 파일이다.
인사 문장을 만드는sayHello함수는default export로 내보낸다.
현재 시간을 구하는getCurrentTime함수는named export로 내보낸다.
app.js는 함수를 가져와 사용하는 파일이다.
greet.js에서 기본 함수와 이름 있는 함수를 가져온 뒤, 결과를display영역에 넣는다.
이번 예제는 함수를 내보내고, 가져오고, 실행 결과를 화면에 출력하는 흐름이다.
예제 전체 코드 1
// exam24.html <!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8"> <title>함수 Default Export 테스트</title> </head> <body> <!-- app.js에서 결과를 넣을 화면 영역이다. --> <div id="display"></div> <!-- 모듈 시스템을 사용하기 위해 type="module"이 필수이다. --> <script type="module" src="./src/app.js"></script> </body> </html>
exam24.html은 화면의 시작점이다.
<div id="display"></div>는app.js가 결과를 넣을 자리이다.
처음에는display영역 안에 아무 내용도 없다.
하지만app.js에서 이 요소를 찾아innerHTML로 내용을 넣으면 화면에 인사 문구와 현재 시간이 출력된다.
아래쪽의script태그에는type="module"이 붙어 있다.
app.js안에서import문법을 사용하기 때문에 반드시module방식으로 불러와야 한다.
import/export를 사용하는JavaScript파일은script type="module"로 실행해야 한다.
예제 전체 코드 2
// greet.js // 함수 선언과 동시에 default export를 한다. export default function sayHello(user) { // 전달받은 user 값을 넣어 인사 문장을 만들어 반환한다. return `안녕하세요, ${user}님! 오늘도 즐거운 코딩 하세요! 🚀`; } // named export이다. 현재 시간을 문자열로 반환한다. export const getCurrentTime = () => new Date().toLocaleTimeString();
greet.js는 두 가지 함수를 내보낸다.
첫 번째는sayHello함수이다.
이 함수는default export로 내보낸다.
sayHello(user)는user값을 받아 인사 문장을 만든다.
예를 들어user값이"개발자"이면"안녕하세요, 개발자님! 오늘도 즐거운 코딩 하세요! 🚀"라는 문자열을 반환한다.
두 번째는getCurrentTime함수이다.
이 함수는named export로 내보낸다.
new Date()로 현재 날짜와 시간 정보를 만들고,toLocaleTimeString()으로 현재 시간을 문자열로 바꿔 반환한다.
greet.js는 기본으로 내보낼 함수 하나와 이름으로 내보낼 함수 하나를 함께 가진다.
예제 전체 코드 3
// app.js // default export는 greet이라는 이름으로 가져온다. // named export인 getCurrentTime은 중괄호 안에 가져온다. import greet, { getCurrentTime } from './greet.js'; // 화면에 넣을 사용자 이름을 변수에 저장한다. const user = "개발자"; // HTML 문서에서 id가 display인 요소를 찾는다. const display = document.getElementById('display'); // display 요소가 존재할 때만 화면 내용을 넣는다. if (display) { // 가져온 함수들의 실행 결과를 HTML 문자열 안에 넣는다. display.innerHTML = ` <h2>${greet(user)}</h2> <p>현재 접속 시간: ${getCurrentTime()}</p> `; }
app.js는greet.js에서 두 함수를 가져온다.
greet는default export로 가져온 함수이다.
getCurrentTime은named export로 가져온 함수이다.
const user = "개발자";는 화면에 넣을 사용자 이름을 준비하는 코드이다.
이 값은 나중에greet(user)를 실행할 때 전달된다.
document.getElementById('display')는exam24.html에서id가display인 요소를 찾는다.
찾은 결과는display변수에 저장된다.
if (display)는display요소가 실제로 존재하는지 확인한다.
요소가 있을 때만innerHTML로 화면 내용을 넣는다.
app.js는 모듈에서 가져온 함수를 실행한 뒤, 그 결과를display영역에 넣는다.
이 예제에서 확인할 핵심
exam24.html은app.js를type="module"로 실행한다.greet.js는sayHello함수를default export로 내보낸다.greet.js는getCurrentTime함수를named export로 내보낸다.app.js는default export를greet이라는 이름으로 가져온다.app.js는named export인getCurrentTime을 중괄호로 가져온다.greet(user)는 사용자 이름이 들어간 인사 문장을 반환한다.getCurrentTime()은 현재 시간을 문자열로 반환한다.document.getElementById('display')는 화면에서 결과를 넣을 위치를 찾는다.if (display)는 해당 요소가 있을 때만 화면 출력을 실행한다.innerHTML은 선택한 요소 안에HTML내용을 넣는다.이 예제는 모듈에서 가져온 함수를 실행하고, 그 결과를 실제 화면에 출력하는 예제이다.
exam24.html에서 출력 위치 준비하기
// exam24.html <!-- app.js에서 결과를 넣을 화면 영역이다. --> <div id="display"></div>이 코드는 화면에 결과가 들어갈 자리를 만든다.
div는 화면 영역을 만드는HTML태그이다.
id="display"는JavaScript에서 이 요소를 찾기 위한 이름이다.
app.js는 나중에document.getElementById('display')를 사용해 이div를 찾는다.
그다음innerHTML로 인사 문장과 현재 시간을 넣는다.
id="display"는JavaScript가 화면 출력 위치를 찾기 위한 기준이다.
script type="module"로 app.js 실행하기
// exam24.html <!-- 모듈 시스템을 사용하기 위해 type="module"이 필수이다. --> <script type="module" src="./src/app.js"></script>이 코드는
app.js를 실행한다.
여기서 중요한 부분은type="module"이다.
app.js안에는import문법이 있다.
import문법은module방식으로 실행되는 파일에서 사용한다.
그래서HTML에서app.js를 불러올 때type="module"을 지정해야 한다.
src="./src/app.js"는 실행할 파일의 위치이다.
현재HTML파일을 기준으로src폴더 안의app.js를 불러온다는 뜻이다.
type="module"은 브라우저가 해당script를 모듈 코드로 해석하게 만드는 설정이다.
default export로 인사 함수 내보내기
// greet.js // 함수 선언과 동시에 default export를 한다. export default function sayHello(user) { // 전달받은 user 값을 넣어 인사 문장을 만들어 반환한다. return `안녕하세요, ${user}님! 오늘도 즐거운 코딩 하세요! 🚀`; }
export default function sayHello(user) { ... }는 함수를 선언하면서 동시에 기본값으로 내보내는 코드이다.
이 함수는greet.js의 대표 내보내기 값이 된다.
user는 매개변수이다.
매개변수는 함수가 호출될 때 전달받은 값을 담는 자리이다.
return은 함수 실행 결과를 밖으로 돌려주는 문법이다.
여기서는template literal을 사용해서 인사 문장을 만든 뒤 반환한다.
예를 들어sayHello("개발자")처럼 호출하면 아래 문자열이 반환된다.// 반환 결과 // 안녕하세요, 개발자님! 오늘도 즐거운 코딩 하세요! 🚀함수도
default export로 내보낼 수 있고, 가져오는 쪽에서 원하는 이름으로 받을 수 있다.
named export로 현재 시간 함수 내보내기
// greet.js // named export이다. 현재 시간을 문자열로 반환한다. export const getCurrentTime = () => new Date().toLocaleTimeString();
getCurrentTime은 현재 시간을 구하는 함수이다.
export const getCurrentTime = ...형태로 작성했기 때문에named export이다.
이 함수는 화살표 함수로 작성되어 있다.
new Date()는 현재 날짜와 시간 정보를 가진 객체를 만든다.
toLocaleTimeString()은 그중 시간 정보를 현재 환경에 맞는 문자열로 바꾼다.
현재 시간은 실행하는 순간마다 달라진다.
그래서 출력 결과를 고정된 값으로 적으면 안 된다.
getCurrentTime()은 실행 시점의 현재 시간을 문자열로 반환한다.
default import와 named import 함께 가져오기
// app.js // default export는 greet이라는 이름으로 가져온다. // named export인 getCurrentTime은 중괄호 안에 가져온다. import greet, { getCurrentTime } from './greet.js';이 코드는
default import와named import를 한 줄에 함께 작성한 것이다.
greet는default export로 내보낸sayHello함수를 받는 이름이다.
default import는 중괄호를 사용하지 않는다.
가져오는 쪽에서 원하는 이름을 붙일 수 있으므로sayHello가 아니라greet이라고 받은 것이다.
{ getCurrentTime }은named export로 내보낸 함수를 가져오는 부분이다.
named import는 중괄호를 사용하고, 내보낸 이름과 맞춰야 한다.
정리하면 아래처럼 볼 수 있다.
greet는 기본으로 가져온 함수이다.getCurrentTime은 이름으로 가져온 함수이다.- 기본 가져오기는 중괄호 밖에 쓴다.
- 이름 있는 가져오기는 중괄호 안에 쓴다.
import greet, { getCurrentTime }은 기본 함수와 이름 있는 함수를 함께 가져오는 코드이다.
user 변수 준비하기
// app.js // 화면에 넣을 사용자 이름을 변수에 저장한다. const user = "개발자";
user변수에는"개발자"라는 문자열이 저장된다.
이 값은 인사 문장을 만들 때 사용된다.
나중에greet(user)가 실행되면user변수의 값이 함수로 전달된다.
즉,greet("개발자")처럼 실행되는 흐름으로 이해하면 된다.
변수에 값을 미리 저장해 두면 함수 호출할 때 그 값을 전달할 수 있다.
getElementById로 display 요소 찾기
// app.js // HTML 문서에서 id가 display인 요소를 찾는다. const display = document.getElementById('display');
document는 현재 브라우저에 열려 있는HTML문서 전체를 의미한다.
getElementById('display')는 그 문서 안에서id가display인 요소를 찾는다.
exam24.html에는<div id="display"></div>가 있다.
그래서 이 코드는 해당div요소를 찾아display변수에 저장한다.
이제display변수를 사용해서 그 영역 안에 내용을 넣을 수 있다.
document.getElementById('display')는 화면에서 결과를 넣을 위치를 찾는 코드이다.
if(display)로 요소 존재 여부 확인하기
// app.js // display 요소가 존재할 때만 화면 내용을 넣는다. if (display) { // 가져온 함수들의 실행 결과를 HTML 문자열 안에 넣는다. display.innerHTML = ` <h2>${greet(user)}</h2> <p>현재 접속 시간: ${getCurrentTime()}</p> `; }
if (display)는display변수에 값이 있는지 확인한다.
즉,id가display인 요소를 제대로 찾았는지 확인하는 코드이다.
만약display요소가 없다면document.getElementById('display')는null을 반환한다.
null은 찾은 값이 없다는 뜻이다.
요소가 없는데display.innerHTML을 실행하면 오류가 날 수 있다.
그래서 먼저if (display)로 요소가 있을 때만 화면 내용을 넣는다.
if (display)는 화면에 내용을 넣기 전에 출력 위치가 실제로 존재하는지 확인하는 안전 장치이다.
innerHTML로 함수 실행 결과 넣기
// app.js // 가져온 함수들의 실행 결과를 HTML 문자열 안에 넣는다. display.innerHTML = ` <h2>${greet(user)}</h2> <p>현재 접속 시간: ${getCurrentTime()}</p> `;
innerHTML은 선택한 요소 안에HTML내용을 넣는 프로퍼티이다.
여기서는display요소 안에h2와p태그를 넣는다.
${greet(user)}는 가져온 기본 함수를 실행한 결과를 문자열 안에 넣는다.
greet(user)는"개발자"를 받아 인사 문장을 반환한다.
${getCurrentTime()}는 이름으로 가져온 함수를 실행한 결과를 문자열 안에 넣는다.
이 함수는 현재 시간을 문자열로 반환한다.
결과적으로display안에는 아래와 같은 구조의HTML이 들어간다.// 화면에 들어가는 HTML 구조 <h2>안녕하세요, 개발자님! 오늘도 즐거운 코딩 하세요! 🚀</h2> <p>현재 접속 시간: 실행 시점의 현재 시간</p>
innerHTML안에서 가져온 함수를 실행하면, 함수의 반환값이 실제 화면 내용으로 들어간다.
화면 출력 결과
이 예제를 실행하면 브라우저 화면에는 인사 문장과 현재 접속 시간이 출력된다.
화면 출력 결과는 아래처럼 볼 수 있다.// 화면 출력 결과 예시 // 안녕하세요, 개발자님! 오늘도 즐거운 코딩 하세요! 🚀 // 현재 접속 시간: 실행 시점의 현재 시간첫 번째 줄은
greet(user)의 결과이다.
user값이"개발자"이므로 인사 문장에"개발자"가 들어간다.
두 번째 줄은getCurrentTime()의 결과이다.
현재 시간은 실행 시점마다 달라진다.
그래서 정확한 시각은 실행할 때마다 다르게 보인다.
고정되는 결과는 인사 문장이고, 현재 시간은 실행 시점에 따라 달라지는 결과이다.
전체 실행 흐름 정리
이 예제의 전체 실행 흐름은 아래처럼 볼 수 있다.
exam24.html이 실행된다.div id="display"가 화면 출력 위치로 준비된다.script type="module"로app.js를 불러온다.app.js가greet.js에서 기본 함수와 이름 있는 함수를 가져온다.greet는default export로 가져온 함수이다.getCurrentTime은named export로 가져온 함수이다.user변수에"개발자"를 저장한다.document.getElementById('display')로 출력 위치를 찾는다.if (display)로 요소가 있는지 확인한다.display.innerHTML에 함수 실행 결과가 들어간HTML문자열을 넣는다.- 화면에 인사 문구와 현재 시간이 출력된다.
이 예제는 가져온 함수를 실행한 결과를
DOM요소의 내용으로 넣는 구조이다.
핵심 정리
이 예제의 핵심은
default export함수와named export함수를 함께 가져와 화면에 사용하는 방법이다.
greet.js는 인사 함수와 현재 시간 함수를 내보내고,app.js는 그 함수들을 가져와display영역에 출력한다.
이 흐름은 아래처럼 정리할 수 있다.
exam24.html은app.js를type="module"로 실행한다.greet.js는sayHello함수를default export로 내보낸다.greet.js는getCurrentTime함수를named export로 내보낸다.app.js는 기본 함수를greet이라는 이름으로 가져온다.app.js는 이름 있는 함수getCurrentTime을 중괄호로 가져온다.greet(user)는 사용자 이름이 들어간 인사 문장을 반환한다.getCurrentTime()은 실행 시점의 현재 시간을 반환한다.document.getElementById('display')는 화면 출력 위치를 찾는다.if (display)는 해당 요소가 실제로 있을 때만 출력하게 한다.innerHTML은 선택한 요소 안에HTML내용을 넣는다.따라서 이 예제가 알려 주는 핵심은 이것이다.
함수도default export와named export로 나누어 내보낼 수 있고, 가져온 함수의 실행 결과는innerHTML을 통해 실제 화면에 출력할 수 있다.
이 예제는 배열 안에 들어 있는 데이터를 조건에 따라 걸러내거나, 다른 형태로 바꾸거나, 하나의 결과로 합치거나, 하나씩 반복 처리하는 흐름을 보여 준다.
배열은 여러 값을 순서대로 담는 구조이다.
이번 예제에서는 단순한 숫자 배열뿐 아니라, 사람 정보를 가진 객체 배열도 함께 사용한다.
여기서 핵심으로 보는 메서드는filter(),map(),reduce(),forEach()이다.
filter()는 조건에 맞는 값만 골라낸다.
map()은 배열의 각 값을 다른 형태로 바꾼다.
reduce()는 배열의 여러 값을 하나의 결과로 누적한다.
forEach()는 배열의 값을 하나씩 꺼내 반복 실행한다.
이 예제의 핵심은 배열 메서드마다 역할이 다르다는 점을 구분하는 것이다.
exam25.html,exam25_1.html,exam25_2.html은 모두filter()를 중심으로 사람 객체 배열에서 점수가80보다 큰 사람만 골라내는 흐름을 보여 준다.
exam25_3.html은map(),reduce(),forEach()를 추가로 사용해서 배열 데이터를 가공하고 출력하는 흐름을 보여 준다.
배열 메서드란 무엇인가
배열 메서드는 배열이 가지고 있는 기능이다.
배열 안의 값을 하나씩 검사하거나, 바꾸거나, 합치거나, 반복 처리할 때 사용한다.
예를 들어 사람 정보가 여러 개 들어 있는 배열이 있다고 해 보자.
이 배열에서 점수가80점보다 큰 사람만 찾고 싶다면 직접 반복문을 작성할 수도 있다.
하지만filter()를 사용하면 조건에 맞는 사람만 더 간단하게 골라낼 수 있다.
배열 메서드는 각각 역할이 다르다.
filter()는 조건에 맞는 요소만 남긴다.map()은 각 요소를 다른 형태로 바꾼 새 배열을 만든다.reduce()는 여러 값을 누적해서 하나의 결과를 만든다.forEach()는 각 요소를 하나씩 꺼내 원하는 작업을 실행한다.배열 메서드는 배열 데이터를 목적에 맞게 처리하기 위한 기능이다.
객체 배열이란 무엇인가
객체 배열은 객체 여러 개가 들어 있는 배열이다.
객체는 여러 값을 하나로 묶은 데이터이다.
이번 예제의 사람 객체는name,point,city를 가진다.
name은 이름이고,point는 점수이고,city는 사는 도시이다.
아래처럼 사람 한 명을 객체로 표현할 수 있다.// exam25.html // 사람 한 명의 정보를 객체로 표현한다. <{ name: "둘리", point: 88, city: "서울" }이런 사람 객체 여러 개를 배열에 넣으면 여러 사람 정보를 하나의 배열로 관리할 수 있다.
객체 배열은 같은 형태의 데이터 여러 개를 하나의 배열로 묶어 관리할 때 사용한다.
예제 전체 코드 1
// exam25.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> </head> <body> <h1>배열의 주요 메서드</h1> <hr> <script src="util.js"></script> <script> // 사람 정보 객체 여러 개를 배열에 저장한다. const persons = [ { name: "둘리", point: 88, city: "서울", }, { name: "또치", point: 92, city: "서울", }, { name: "도우너", point: 76, city: "제주", }, { name: "고길동", point: 78, city: "서울", } ]; // 전체 사람 배열을 화면에 출력한다. write(persons, 'h3'); // 화면에 구분선을 출력한다. hr(); // 점수가 80보다 큰 사람만 골라 새 배열로 만든다. const pass = persons.filter(function (person) { return person.point > 80; }); // 조건을 통과한 사람 배열을 빨간색 h2 태그로 출력한다. writeColor(pass, 'h2', 'red'); </script> </body> </html>
exam25.html은 객체 배열과filter()의 기본 흐름을 보여 준다.
persons배열에는 사람 객체 네 개가 들어 있다.
각 객체는name,point,city값을 가진다.
filter()는persons배열의 요소를 하나씩 검사한다.
여기서는person.point > 80조건을 사용한다.
점수가80보다 큰 사람만pass배열에 들어간다.
조건을 통과하는 사람은"둘리"와"또치"이다.
"도우너"는76점이고,"고길동"은78점이므로 제외된다.
exam25.html의 핵심은 객체 배열에서 조건에 맞는 객체만 새 배열로 골라내는 것이다.
예제 전체 코드 2
// exam25_1.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> </head> <body> <h1>배열의 주요 메서드</h1> <hr> <script src="util.js"></script> <script> // 사람 정보 객체 여러 개를 배열에 저장한다. const persons = [ { name: "둘리", point: 88, city: "서울", // 객체가 문자열로 표현될 때 name 값이 보이게 한다. toString: function () { return this.name; } }, { name: "또치", point: 92, city: "서울", // 객체가 문자열로 표현될 때 name 값이 보이게 한다. toString: function () { return this.name; } }, { name: "도우너", point: 76, city: "제주", // 객체가 문자열로 표현될 때 name 값이 보이게 한다. toString: function () { return this.name; } }, { name: "고길동", point: 78, city: "서울", // 객체가 문자열로 표현될 때 name 값이 보이게 한다. toString: function () { return this.name; } }, ]; // 전체 사람 배열을 화면에 출력한다. write(persons, 'h3'); // 화면에 구분선을 출력한다. hr(); // 점수가 80보다 큰 사람만 골라 새 배열로 만든다. const pass = persons.filter(function (person) { return person.point > 80; }); // 조건을 통과한 사람 배열을 빨간색 h3 태그로 출력한다. writeColor(pass, 'h3', 'red'); </script> </body> </html>
exam25_1.html도filter()흐름은exam25.html과 같다.
차이는 각 사람 객체 안에toString함수가 들어 있다는 점이다.
toString은 객체가 문자열처럼 표현될 때 어떤 값을 보여 줄지 정하는 함수이다.
여기서는return this.name;을 사용한다.
그래서 객체가 화면에 출력될 때 객체 전체가 복잡하게 보이는 대신 사람 이름이 보이게 된다.
객체 안에toString을 직접 넣으면 객체를 문자열로 출력할 때 보여 줄 값을 정할 수 있다.
예제 전체 코드 3
// exam25_2.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> </head> <body> <h1>배열의 주요 메서드</h1> <hr> <script src="util.js"></script> <script> // 사람 객체를 만들기 위한 생성자 함수이다. function Person(name, point, city) { // 전달받은 name을 현재 객체에 저장한다. this.name = name; // 전달받은 point를 현재 객체에 저장한다. this.point = point; // 전달받은 city를 현재 객체에 저장한다. this.city = city; } // Person으로 만든 객체들이 공통으로 사용할 toString 함수를 등록한다. Person.prototype.toString = function () { return this.name; }; // Person 생성자 함수로 사람 객체 여러 개를 만든 뒤 배열에 저장한다. const persons = [ new Person("둘리", 88, "서울"), new Person("또치", 92, "서울"), new Person("도우너", 76, "제주"), new Person("고길동", 78, "서울") ]; // 전체 사람 배열을 화면에 출력한다. write(persons, 'h3'); // 화면에 구분선을 출력한다. hr(); // 점수가 80보다 큰 사람만 골라 새 배열로 만든다. const pass = persons.filter(function (person) { return person.point > 80; }); // 조건을 통과한 사람 배열을 빨간색 h3 태그로 출력한다. writeColor(pass, 'h3', 'red'); </script> </body> </html>
exam25_2.html은 사람 객체를 객체 리터럴로 직접 만들지 않는다.
대신Person생성자 함수를 사용한다.
생성자 함수는 같은 구조의 객체를 여러 개 만들 때 사용하는 함수이다.
new Person("둘리", 88, "서울")을 실행하면name,point,city를 가진 사람 객체가 만들어진다.
또Person.prototype.toString을 사용한다.
이렇게 하면Person생성자 함수로 만든 객체들이 공통으로toString함수를 사용할 수 있다.
객체마다toString을 직접 넣는 방식보다 코드 반복을 줄일 수 있다.
prototype에 함수를 등록하면 같은 생성자 함수로 만든 객체들이 공통 기능을 함께 사용할 수 있다.
예제 전체 코드 4
// exam25_3.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> </head> <body> <h1>배열의 주요 메서드</h1> <hr> <script src="util.js"></script> <script> // 사람 객체를 만들기 위한 생성자 함수이다. function Person(name, point, city) { // 전달받은 name을 현재 객체에 저장한다. this.name = name; // 전달받은 point를 현재 객체에 저장한다. this.point = point; // 전달받은 city를 현재 객체에 저장한다. this.city = city; } // Person으로 만든 객체들이 공통으로 사용할 toString 함수를 등록한다. Person.prototype.toString = function () { return this.name; }; // Person 생성자 함수로 사람 객체 여러 개를 만든 뒤 배열에 저장한다. const persons = [ new Person("둘리", 88, "서울"), new Person("또치", 92, "서울"), new Person("도우너", 76, "제주"), new Person("고길동", 78, "서울") ]; // 사람 객체 배열을 이름(점수) 형태의 문자열 배열로 바꾼다. const plist = persons.map(function (p) { return `${p.name}(${p.point})`; // ES6 : 템플릿리터럴 p.name+"("+p.point+")" }); // 변환된 문자열 배열을 파란색 h3 태그로 출력한다. writeColor(plist, 'h3', 'blue'); // 숫자 배열을 만든다. const points = [40, 100, 1, 5, 25, 10]; // 숫자 배열의 값을 모두 더한다. const sum = points.reduce(function (total, currentValue) { return total + currentValue; }, 0); // 합계를 초록색 h2 태그로 출력한다. writeColor(sum, 'h2', 'green'); // 화면에 구분선을 출력한다. hr(); // 숫자 배열의 각 값을 하나씩 꺼내 순서와 함께 출력한다. points.forEach(function (e, i) { writeColor((i + 1) + "번 데이터 " + e + "!!", 'h3', 'magenta'); }) </script> </body> </html>
exam25_3.html은filter()대신map(),reduce(),forEach()를 확인한다.
persons.map(...)은 사람 객체 배열을"이름(점수)"형태의 문자열 배열로 바꾼다.
points.reduce(...)는 숫자 배열의 값을 모두 더해 하나의 합계를 만든다.
points.forEach(...)는 숫자 배열의 값을 하나씩 꺼내 화면에 출력한다.
exam25_3.html은 배열 데이터를 변환하고, 누적하고, 반복 출력하는 메서드 흐름을 보여 준다.
이 예제에서 확인할 핵심
filter()는 조건에 맞는 요소만 골라 새 배열을 만든다.map()은 배열의 각 요소를 다른 형태로 바꿔 새 배열을 만든다.reduce()는 배열의 값을 누적해서 하나의 결과를 만든다.forEach()는 배열의 각 요소를 하나씩 꺼내 반복 처리한다.- 객체 배열은 사람 정보처럼 구조가 같은 데이터를 여러 개 관리할 때 사용한다.
toString은 객체가 문자열로 표현될 때 사용할 값을 정한다.prototype은 같은 생성자 함수로 만든 객체들이 공통 기능을 공유하게 한다.이 예제는 배열 메서드를 사용해 객체 배열과 숫자 배열을 목적에 맞게 처리하는 방법을 보여 준다.
filter로 조건에 맞는 사람만 고르기
// exam25.html // 점수가 80보다 큰 사람만 골라 새 배열로 만든다. const pass = persons.filter(function (person) { return person.point > 80; });
filter()는 배열의 요소를 하나씩 꺼내 조건을 검사한다.
여기서person은 현재 검사 중인 사람 객체이다.
person.point > 80은 현재 사람의 점수가80보다 큰지 확인한다.
결과가true이면 현재 사람 객체가 새 배열에 들어간다.
결과가false이면 새 배열에 들어가지 않는다.
검사 흐름은 아래처럼 볼 수 있다.
"둘리"는88 > 80이므로 통과한다."또치"는92 > 80이므로 통과한다."도우너"는76 > 80이 아니므로 제외된다."고길동"은78 > 80이 아니므로 제외된다.따라서
pass배열에는"둘리"와"또치"객체만 들어간다.
filter()의 콜백 함수가true를 반환한 요소만 결과 배열에 남는다.
filter는 원본 배열을 바꾸지 않는다
filter()는 조건에 맞는 요소만 모아 새 배열을 만든다.
원본 배열인persons를 직접 삭제하거나 수정하지 않는다.
이 예제에서pass에는"둘리"와"또치"만 들어간다.
하지만persons에는 여전히"둘리","또치","도우너","고길동"이 모두 남아 있다.
정리하면 아래처럼 볼 수 있다.
persons는 원본 배열이다.pass는 조건을 통과한 객체만 모은 새 배열이다.filter()는 원본 배열을 직접 바꾸지 않는다.원본 데이터를 유지하면서 조건에 맞는 목록만 따로 만들고 싶을 때
filter()를 사용한다.
객체를 출력할 때 toString이 필요한 이유
객체는 여러 값을 가진 데이터 묶음이다.
그런데 객체를 그대로 문자열처럼 출력하면 사람이 읽기 좋은 형태로 보이지 않을 수 있다.
exam25.html은 객체 안에toString을 따로 정의하지 않는다.
그래서 객체 배열을 화면에 출력하면 객체의 기본 문자열 표현이 보일 수 있다.
중요한 점은 객체의 값이 사라진 것이 아니라는 점이다.
단지 객체를 문자열로 보여 주는 방법을 따로 정하지 않았을 뿐이다.
exam25_1.html은 각 객체에toString함수를 넣는다.// exam25_1.html // 객체가 문자열로 표현될 때 name 값이 보이게 한다. toString: function () { return this.name; }이 함수는 현재 객체의
name값을 반환한다.
그래서 사람 객체가 문자열처럼 출력될 때"둘리","또치"처럼 이름이 보이게 된다.
toString은 객체를 문자열로 표현할 때 어떤 값을 보여 줄지 정하는 함수이다.
객체마다 toString을 직접 넣는 방식
exam25_1.html에서는 각 객체 안에toString함수가 직접 들어 있다.
이 방식은 이해하기 쉽다.
객체를 만들 때 그 객체가 문자열로 보일 방식을 같이 적기 때문이다.
하지만 같은 함수가 객체마다 반복된다.
사람 객체가 네 개이므로toString: function () { return this.name; }도 네 번 작성된다.
정리하면 아래처럼 볼 수 있다.
- 각 객체가 자기
toString함수를 가진다.- 객체를 문자열로 표현할 때
name을 반환한다.- 이해하기 쉽지만 같은 코드가 반복된다.
객체마다
toString을 직접 넣는 방식은 단순하지만, 같은 기능이 여러 객체에 반복될 수 있다.
prototype으로 toString 공유하기
// exam25_2.html // 사람 객체를 만들기 위한 생성자 함수이다. function Person(name, point, city) { this.name = name; this.point = point; this.city = city; } // Person으로 만든 객체들이 공통으로 사용할 toString 함수를 등록한다. Person.prototype.toString = function () { return this.name; };
exam25_2.html은Person생성자 함수를 사용한다.
생성자 함수는 같은 구조의 객체를 여러 개 만들 때 사용한다.
Person.prototype.toString은Person으로 만든 객체들이 공통으로 사용할 함수를 등록한다.
이렇게 하면 객체마다toString함수를 직접 넣지 않아도 된다.
new Person("둘리", 88, "서울")로 만든 객체는 자기 안에name,point,city를 가진다.
그리고 문자열로 표현될 때는Person.prototype에 있는toString을 사용할 수 있다.
prototype은 같은 생성자 함수로 만든 객체들이 공통 메서드를 공유하게 하는 구조이다.
map으로 객체 배열을 문자열 배열로 바꾸기
// exam25_3.html // 사람 객체 배열을 이름(점수) 형태의 문자열 배열로 바꾼다. const plist = persons.map(function (p) { return `${p.name}(${p.point})`; // ES6 : 템플릿리터럴 p.name+"("+p.point+")" }); writeColor(plist, 'h3', 'blue');
map()은 배열의 각 요소를 다른 형태로 바꾸어 새 배열을 만든다.
여기서 원본 배열persons의 각 요소는 사람 객체이다.
map()안에서p는 현재 처리 중인 사람 객체이다.
return{p.point});는 사람 객체를"이름(점수)"형태의 문자열로 바꾼다.
변환 흐름은 아래처럼 볼 수 있다.
- 둘리 객체는
"둘리(88)"이 된다.- 또치 객체는
"또치(92)"가 된다.- 도우너 객체는
"도우너(76)"이 된다.- 고길동 객체는
"고길동(78)"이 된다.따라서
plist는 사람 객체 배열이 아니라 문자열 배열이다.
map()은 배열의 각 요소를 변환해서 같은 개수의 새 배열을 만든다.
reduce로 숫자 배열의 합계 구하기
// exam25_3.html // 숫자 배열을 만든다. const points = [40, 100, 1, 5, 25, 10]; // 숫자 배열의 값을 모두 더한다. const sum = points.reduce(function (total, currentValue) { return total + currentValue; }, 0); writeColor(sum, 'h2', 'green');
reduce()는 배열의 값을 누적해서 하나의 결과를 만든다.
여기서는 숫자 배열의 값을 모두 더해 합계를 만든다.
total은 지금까지 누적된 값이다.
currentValue는 현재 처리 중인 배열 요소이다.
마지막의0은 누적을 시작할 초기값이다.
계산 흐름은 아래처럼 볼 수 있다.
- 처음
total은0이다.0 + 40으로40이 된다.40 + 100으로140이 된다.140 + 1로141이 된다.141 + 5로146이 된다.146 + 25로171이 된다.171 + 10으로181이 된다.그래서
sum에는181이 저장된다.
reduce()는 배열의 여러 값을 하나의 결과로 누적할 때 사용한다.
forEach로 배열 요소를 하나씩 출력하기
// exam25_3.html // 숫자 배열의 각 값을 하나씩 꺼내 순서와 함께 출력한다. points.forEach(function (e, i) { writeColor((i + 1) + "번 데이터 " + e + "!!", 'h3', 'magenta'); })
forEach()는 배열의 요소를 하나씩 꺼내면서 지정한 함수를 실행한다.
여기서e는 현재 꺼낸 값이다.
i는 현재 값의 인덱스이다.
인덱스는0부터 시작한다.
하지만 화면에는1번 데이터,2번 데이터처럼 보여 주고 싶다.
그래서(i + 1)을 사용한다.
출력 흐름은 아래처럼 볼 수 있다.
- 첫 번째 값
40은1번 데이터 40!!으로 출력된다.- 두 번째 값
100은2번 데이터 100!!으로 출력된다.- 세 번째 값
1은3번 데이터 1!!으로 출력된다.- 네 번째 값
5는4번 데이터 5!!으로 출력된다.- 다섯 번째 값
25는5번 데이터 25!!으로 출력된다.- 여섯 번째 값
10은6번 데이터 10!!으로 출력된다.
forEach()는 배열의 각 요소를 순서대로 꺼내 반복 작업을 할 때 사용한다.
filter, map, reduce, forEach 차이 비교하기
네 메서드는 모두 배열의 요소를 다룬다.
하지만 목적이 다르다.
filter()는 조건에 맞는 요소만 남긴다.
결과는 조건을 통과한 요소들로 이루어진 새 배열이다.
map()은 각 요소를 다른 형태로 바꾼다.
결과는 원본 배열과 같은 개수의 새 배열이다.
reduce()는 여러 요소를 누적해 하나의 결과로 만든다.
결과는 배열이 아니라 합계 같은 하나의 값일 수 있다.
forEach()는 각 요소를 하나씩 꺼내 작업만 실행한다.
반환 결과를 모아 새 배열을 만드는 목적이 아니다.
정리하면 아래처럼 볼 수 있다.
filter()는 조건으로 골라낸다.map()은 형태를 바꾼다.reduce()는 하나로 누적한다.forEach()는 하나씩 반복 실행한다.배열 메서드를 고를 때는 “걸러낼지, 바꿀지, 합칠지, 반복 실행할지”를 먼저 생각하면 된다.
화면 출력 결과 흐름
exam25_1.html과exam25_2.html은toString을 통해 사람 이름이 출력되도록 만든다.
그래서 전체 배열과filter()결과를 이름 중심으로 확인할 수 있다.
화면 출력 흐름은 아래처럼 볼 수 있다.// 화면 출력 결과 // 둘리,또치,도우너,고길동 // 둘리,또치첫 번째 줄은 전체
persons배열을 출력한 흐름이다.
두 번째 줄은filter()를 통과한pass배열을 출력한 흐름이다.
exam25_3.html의 화면 출력 흐름은 아래처럼 볼 수 있다.// 화면 출력 결과 // 둘리(88),또치(92),도우너(76),고길동(78) // 181 // 1번 데이터 40!! // 2번 데이터 100!! // 3번 데이터 1!! // 4번 데이터 5!! // 5번 데이터 25!! // 6번 데이터 10!!첫 번째 줄은
map()으로 만든 문자열 배열이다.
두 번째 줄은reduce()로 구한 합계이다.
그다음 줄들은forEach()로 숫자 배열의 요소를 하나씩 출력한 결과이다.
출력 결과를 보면 각 배열 메서드가 어떤 방식으로 데이터를 처리하는지 한눈에 확인할 수 있다.
핵심 정리
이 예제의 핵심은 배열 데이터를 목적에 맞게 처리하는 메서드의 차이이다.
filter(),map(),reduce(),forEach()는 모두 배열을 다루지만 결과와 목적이 다르다.
이 흐름은 아래처럼 정리할 수 있다.
filter()는 조건에 맞는 요소만 골라 새 배열을 만든다.person.point > 80조건을 통과하는 사람은"둘리"와"또치"이다.toString을 정의하면 객체가 문자열로 표현될 때 보여 줄 값을 정할 수 있다.- 객체마다
toString을 직접 넣을 수도 있다.prototype에toString을 등록하면 같은 생성자 함수로 만든 객체들이 공통으로 사용할 수 있다.map()은 배열의 각 요소를 다른 형태로 바꾼 새 배열을 만든다.persons.map(...)은 사람 객체를"이름(점수)"문자열로 바꾼다.reduce()는 배열의 값을 누적해서 하나의 결과를 만든다.points.reduce(...)의 합계는181이다.forEach()는 배열의 각 요소를 하나씩 꺼내 반복 작업을 실행한다.따라서 이 예제가 알려 주는 핵심은 이것이다.
배열 메서드는 목적에 따라 골라 써야 하며,filter()는 조건으로 걸러내고,map()은 변환하고,reduce()는 누적하고,forEach()는 반복 실행한다.
이 예제는
setTimeout()이 비동기 방식으로 동작할 때 코드 작성 순서와 실제 실행 순서가 어떻게 달라지는지 보여 준다.
비동기는 어떤 작업이 끝날 때까지 전체 코드 실행을 멈추지 않고, 다음 코드를 먼저 실행할 수 있게 하는 처리 방식이다.
이 예제에서는work()함수 안에서"작업 시작"을 먼저 출력한다.
그다음setTimeout()으로"작업 완료"를 2초 뒤에 출력하도록 예약한다.
하지만setTimeout()이 끝날 때까지 기다리지 않고, 바로 아래 코드인"다음 작업"이 먼저 실행된다.
이 예제의 핵심은setTimeout()안의 코드가 바로 실행되는 것이 아니라, 지정한 시간이 지난 뒤 나중에 실행된다는 점이다.
그래서 코드가 위에서 아래로 작성되어 있어도 실제 출력 순서는 다르게 나타난다.
예제 전체 코드
// exam26.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> </head> <body> <h1>setTimeout() 비동기 처리 함수라서...</h1> <script> function work() { // 첫 번째로 바로 실행되는 코드이다. console.log("작업 시작"); // 2초 뒤에 실행될 코드를 예약한다. setTimeout(() => { console.log("작업 완료"); }, 2000); // setTimeout()이 끝날 때까지 기다리지 않고 바로 실행된다. console.log("다음 작업"); } // work 함수를 호출한다. work(); </script> </body> </html>이 코드는 크게 세 부분으로 나누어 볼 수 있다.
첫 번째는work()함수를 정의하는 부분이다.
두 번째는setTimeout()으로 2초 뒤 실행할 코드를 예약하는 부분이다.
세 번째는work()함수를 호출해서 실제 실행 흐름을 확인하는 부분이다.
여기서 가장 중요한 코드는setTimeout()이다.
setTimeout()은 일정 시간이 지난 뒤 특정 코드를 실행하도록 예약하는 함수이다.
이 예제에서는2000을 전달했기 때문에 약 2초 뒤에"작업 완료"가 출력된다.
하지만setTimeout()은 2초 동안 코드 전체를 멈추지 않는다.
그래서setTimeout()아래에 있는console.log("다음 작업")이 먼저 실행된다.
setTimeout()은 나중에 실행할 작업을 예약하고, 현재 코드 흐름은 계속 진행시킨다.
비동기란 무엇인가
비동기는 어떤 작업이 끝날 때까지 기다리지 않고, 다음 코드를 먼저 실행할 수 있게 하는 처리 방식이다.
쉽게 말하면 오래 걸릴 수 있는 작업을 잠시 맡겨 두고, 현재 코드는 멈추지 않고 계속 진행하는 방식이다.
일반 코드는 보통 위에서 아래로 순서대로 실행된다.
이런 흐름을 동기 처리라고 볼 수 있다.
동기 처리는 앞의 작업이 끝나야 다음 작업으로 넘어간다.
반면 비동기 처리는 오래 걸리는 작업을 만났다고 해서 전체 흐름을 멈추지 않는다.
먼저 실행할 수 있는 코드는 계속 실행하고, 시간이 필요한 작업은 나중에 완료되었을 때 따로 처리한다.
이 예제에서는setTimeout()이 비동기 흐름을 만든다.
setTimeout()은"작업 완료"를 바로 출력하지 않고, 2초 뒤에 실행되도록 예약한다.
그동안JavaScript는 기다리지 않고 바로 아래 코드인"다음 작업"을 먼저 실행한다.
정리하면 아래처럼 볼 수 있다.
- 동기 처리는 앞 작업이 끝나야 다음 작업을 실행한다.
- 비동기 처리는 오래 걸리는 작업을 기다리지 않고 다음 코드를 먼저 실행할 수 있다.
setTimeout()은 나중에 실행할 코드를 예약하는 대표적인 비동기 처리 함수이다.- 그래서 코드 작성 순서와 실제 출력 순서가 달라질 수 있다.
비동기는 작업을 순서대로 안 한다는 뜻이 아니라, 시간이 걸리는 작업을 기다리는 동안 다른 코드를 먼저 실행할 수 있게 하는 처리 방식이다.
이 예제에서 확인할 핵심
setTimeout()은 일정 시간이 지난 뒤 코드를 실행한다.setTimeout()안의 코드는 바로 실행되지 않는다.2000은 약 2초 뒤에 실행하겠다는 의미이다.console.log("작업 시작")은 바로 실행된다.console.log("다음 작업")도 바로 실행된다.console.log("작업 완료")는 2초 뒤에 실행된다.- 그래서 출력 순서는 코드 작성 순서와 다르게 보인다.
이 예제는 비동기 코드가 들어가면 코드의 위치만 보고 실행 순서를 판단하면 안 된다는 점을 보여 준다.
work 함수는 실행 흐름을 확인하기 위한 함수이다
// exam26.html function work() { // 첫 번째로 바로 실행되는 코드이다. console.log("작업 시작"); // 2초 뒤에 실행될 코드를 예약한다. setTimeout(() => { console.log("작업 완료"); }, 2000); // setTimeout()이 끝날 때까지 기다리지 않고 바로 실행된다. console.log("다음 작업"); } work(); // work 함수를 실행한다.
work()함수는 비동기 실행 순서를 확인하기 위한 함수이다.
함수 안에는 출력 코드가 세 개 있다.
첫 번째 출력 코드는console.log("작업 시작")이다.
이 코드는 일반 코드이기 때문에work()가 실행되면 바로 실행된다.
두 번째 출력 코드는setTimeout()안에 들어 있다.
이 코드는 바로 실행되지 않고, 2초 뒤에 실행되도록 예약된다.
세 번째 출력 코드는console.log("다음 작업")이다.
이 코드는setTimeout()아래에 있지만, 2초를 기다리지 않고 바로 실행된다.
즉,work()함수 안에서 코드는 위에서 아래로 읽히지만,setTimeout()안의 코드는 나중에 실행된다.
함수 안에setTimeout()이 있어도, 그 아래 코드가setTimeout()완료를 기다리는 것은 아니다.
setTimeout은 나중에 실행할 코드를 예약한다
// exam26.html setTimeout(() => { // 2초 뒤에 실행된다. console.log("작업 완료"); }, 2000);
setTimeout()은 두 가지 값을 받는다.
첫 번째 값은 나중에 실행할 함수이다.
두 번째 값은 몇 밀리초 뒤에 실행할지 정하는 시간이다.
여기서() => { ... }는 화살표 함수이다.
이 함수 안에는console.log("작업 완료")가 들어 있다.
즉, 나중에 실행될 실제 작업은"작업 완료"를 출력하는 코드이다.
두 번째 값인2000은2000밀리초를 의미한다.
1000밀리초가 1초이므로,2000밀리초는 약 2초이다.
그래서 이 코드는 “2초 뒤에"작업 완료"를 출력해라”라는 의미가 된다.
중요한 점은setTimeout()이 2초 동안 현재 코드 실행을 멈추지 않는다는 것이다.
나중에 실행할 작업만 예약하고, 현재 흐름은 계속 다음 줄로 내려간다.
실제 출력 순서 확인하기
이 예제를 실행하면
Console에는 아래 순서로 출력된다.// 출력결과 // 작업 시작 // 다음 작업 // 작업 완료첫 번째로
"작업 시작"이 출력된다.
이 코드는work()함수가 실행되자마자 바로 실행되기 때문이다.
그다음setTimeout()을 만나면"작업 완료"출력이 2초 뒤로 예약된다.
하지만 현재 코드 흐름은 멈추지 않는다.
그래서 바로 아래에 있는"다음 작업"이 두 번째로 출력된다.
마지막으로 약 2초가 지난 뒤"작업 완료"가 출력된다.
출력 순서는작업 시작→다음 작업→작업 완료이다.
코드상으로는"작업 완료"가"다음 작업"보다 위에 있지만, 실제 실행은 더 늦게 된다.
결과물 뽑기
이 예제의 결과물은 브라우저 화면보다 개발자 도구
Console에서 확인하는 것이 중요하다.
브라우저 화면에는 제목만 보이고, 실제 실행 순서는Console출력으로 확인해야 한다.
먼저exam26.html을 브라우저에서 실행한다.
그다음 개발자 도구를 열고Console탭을 확인한다.
그러면 아래 순서로 로그가 출력된다.// 출력결과 // 작업 시작 // 다음 작업 // 작업 완료
setTimeout()안의 코드는 2초 뒤 실행되기 때문에,작업 완료보다다음 작업이 먼저 출력된다.
이 결과에서 가장 중요한 부분은"작업 완료"의 위치이다.
코드에서는"작업 완료"출력 코드가"다음 작업"보다 먼저 작성되어 있다.
하지만 실제 결과에서는"다음 작업"이 먼저 출력된다.
이 결과를 통해setTimeout()안의 코드는 바로 실행되는 코드가 아니라, 나중에 실행되도록 예약된 코드라는 점을 확인할 수 있다.
이 예제의 최종 결과물은Console에서작업 시작→다음 작업→작업 완료순서가 보이는 화면이다.
동기 코드와 비동기 코드 구분하기
이 예제를 이해하려면 동기 코드와 비동기 코드를 구분해야 한다.
동기 코드는 앞 코드가 끝난 뒤 다음 코드가 실행되는 방식이다.
일반적인console.log()는 동기 코드처럼 바로 실행된다.
반면 비동기 코드는 작업이 끝날 때까지 전체 실행 흐름을 붙잡지 않는다.
setTimeout()은 대표적인 비동기 처리 함수이다.
지정한 시간이 지난 뒤 실행할 작업을 예약하고, 현재 코드는 계속 다음 줄로 진행된다.
이 예제에서는 아래처럼 구분할 수 있다.
console.log("작업 시작")은 바로 실행되는 동기 코드이다.setTimeout(...)안의console.log("작업 완료")는 나중에 실행되는 비동기 코드이다.console.log("다음 작업")은 다시 바로 실행되는 동기 코드이다.그래서 동기 코드인
"작업 시작"과"다음 작업"이 먼저 실행되고, 비동기로 예약된"작업 완료"가 나중에 실행된다.
비동기 코드는 코드의 위치가 앞에 있어도 실제 실행은 나중에 될 수 있다.
핵심 정리
이 예제의 핵심은
setTimeout()이 비동기 방식으로 동작한다는 점이다.
setTimeout()안의 코드는 바로 실행되지 않고, 지정한 시간이 지난 뒤 실행된다.
이 흐름은 아래처럼 정리할 수 있다.
work()함수가 실행되면"작업 시작"이 바로 출력된다.setTimeout()은"작업 완료"를 2초 뒤 실행하도록 예약한다.setTimeout()은 2초 동안 전체 코드 실행을 멈추지 않는다.- 그래서
"다음 작업"이 먼저 출력된다.- 약 2초 뒤
"작업 완료"가 출력된다.- 최종 출력 순서는
작업 시작→다음 작업→작업 완료이다.- 브라우저 화면보다 개발자 도구
Console에서 실행 순서를 확인해야 한다.따라서 이 예제가 알려 주는 핵심은 이것이다.
setTimeout()은 나중에 실행할 코드를 예약하는 비동기 처리 함수이므로, 코드 작성 순서와 실제 출력 순서가 달라질 수 있다.
다음 예제에서는 이런 비동기 작업의 결과를Promise로 어떻게 이어서 처리하는지 확인한다.
이 예제는
Promise를 사용해서 비동기 작업이 끝난 뒤 다음 작업을 이어서 처리하는 흐름을 보여 준다.
앞의 예제에서는setTimeout()때문에"다음 작업"이"작업 완료"보다 먼저 출력되었다.
이번 예제에서는Promise를 사용해서"작업 완료"가 끝난 뒤"다음 작업"이 실행되도록 흐름을 제어한다.
Promise는 지금 당장 결과가 없지만, 나중에 작업이 끝나면 성공 결과나 실패 결과를 알려주는 객체이다.
비동기 작업은 바로 결과가 나오지 않을 수 있다.
그래서 비동기 작업이 끝난 뒤 실행할 코드를 따로 연결할 방법이 필요하다.
이 예제의 핵심은Promise를 사용하면 비동기 작업이 끝난 뒤then()과finally()로 다음 흐름을 이어서 처리할 수 있다는 점이다.
예제 전체 코드
// exam26_1.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> </head> <body> <h1>Promise 객체 - 나중에 결과를 줄께!!</h1> <hr> <h2>Promise 객체를 사용하는 이유 : 비동기 작업 순서를 깔끔하게 처리하기 위해</h2> <h3>Promise 는 지금 바로 결과는 없지만, 작업이 끝나면 결과를 알려주겠다는 객체</h3> <script> function work() { // Promise 객체를 만들어 반환한다. return new Promise((resolve, reject) => { // Promise가 만들어지면 바로 실행되는 코드이다. console.log("작업 시작"); // 2초 뒤에 성공 결과를 전달한다. setTimeout(() => { resolve("작업 완료"); }, 2000); }); } // work()가 반환한 Promise의 성공 결과를 처리한다. work() .then((result) => { // resolve()가 전달한 값을 출력한다. console.log(result); }).finally(() => { // 성공 또는 실패와 관계없이 마지막에 실행된다. console.log("다음 작업"); }) </script> </body> </html>이 코드는 크게 세 부분으로 나누어 볼 수 있다.
첫 번째는work()함수가Promise객체를 만들어 반환하는 부분이다.
두 번째는setTimeout()안에서 2초 뒤resolve("작업 완료")를 호출하는 부분이다.
세 번째는work()가 반환한Promise에then()과finally()를 연결하는 부분이다.
앞의 예제에서는setTimeout()이"작업 완료"를 2초 뒤로 예약하고, 바로"다음 작업"을 실행했다.
그래서 출력 순서는작업 시작→다음 작업→작업 완료였다.
이번 예제에서는"다음 작업"을 일반 코드로 바로 작성하지 않는다.
대신finally()안에 넣는다.
그래서Promise작업이 처리된 뒤 마지막에"다음 작업"이 실행된다.
Promise를 사용하면 비동기 작업이 끝난 뒤 실행할 코드를 연결할 수 있다.
Promise란 무엇인가
Promise는 비동기 작업의 결과를 나중에 받기 위한 객체이다.
이름 그대로 “지금 바로 결과를 줄 수는 없지만, 작업이 끝나면 결과를 알려 주겠다”는 약속처럼 이해하면 된다.
일반 함수는 보통 실행되면 바로 값을 반환한다.
하지만 비동기 작업은 바로 결과를 만들 수 없는 경우가 많다.
예를 들어 일정 시간 뒤 실행되는 작업, 서버에서 데이터를 받아오는 작업, 파일을 읽는 작업처럼 시간이 걸리는 작업은 바로 결과가 나오지 않는다.
이럴 때Promise를 사용하면 작업이 끝난 뒤의 흐름을 정리할 수 있다.
성공하면resolve()로 결과를 전달하고, 실패하면reject()로 실패 이유를 전달할 수 있다.
이 예제에서는 실패 흐름을 사용하지 않는다.
그래서reject매개변수는 있지만 실제로 호출하지 않는다.
대신 2초 뒤resolve("작업 완료")를 호출해서 성공 결과만 전달한다.
정리하면 아래처럼 볼 수 있다.
Promise는 비동기 작업의 결과를 나중에 받기 위한 객체이다.resolve()는 작업이 성공했을 때 결과를 전달한다.reject()는 작업이 실패했을 때 실패 이유를 전달한다.then()은resolve()로 전달된 성공 결과를 처리한다.catch()는reject()로 전달된 실패 결과를 처리한다.finally()는 성공과 실패에 관계없이 마지막에 실행된다.
Promise는 비동기 작업이 끝난 다음에 무엇을 할지 연결하기 위해 사용한다.
work 함수는 Promise 객체를 반환한다
// exam26_1.html function work() { // Promise 객체를 만들어 반환한다. return new Promise((resolve, reject) => { console.log("작업 시작"); // Promise가 만들어질 때 바로 실행된다. setTimeout(() => { resolve("작업 완료"); // 2초 뒤 성공 결과를 전달한다. }, 2000); }); }
work()함수는Promise객체를 반환한다.
반환한다는 것은 함수 실행 결과로 어떤 값을 밖으로 내보낸다는 뜻이다.
여기서는 숫자나 문자열을 바로 반환하는 것이 아니라,Promise객체를 반환한다.
new Promise(...)는 새로운Promise객체를 만드는 코드이다.
Promise안에는 함수 하나가 들어간다.
이 함수는resolve와reject를 매개변수로 받는다.
resolve는 비동기 작업이 성공했을 때 호출하는 함수이다.
reject는 비동기 작업이 실패했을 때 호출하는 함수이다.
이번 예제에서는 작업 실패 상황을 만들지 않기 때문에reject는 사용하지 않는다.
Promise안의console.log("작업 시작")은 바로 실행된다.
즉,Promise객체가 만들어지는 순간"작업 시작"이 출력된다.
그다음setTimeout()이"작업 완료"라는 성공 결과를 2초 뒤에 전달하도록 예약한다.
이때"작업 완료"를 직접console.log()로 출력하지 않고,resolve()에 전달한다는 점이 중요하다.
work()는 비동기 작업을 담은Promise를 반환하고, 2초 뒤resolve()로 성공 결과를 전달한다.
resolve는 성공 결과를 전달한다
// exam26_1.html setTimeout(() => { resolve("작업 완료"); // Promise의 성공 결과로 문자열을 전달한다. }, 2000);
resolve()는Promise작업이 성공적으로 끝났을 때 호출하는 함수이다.
이 예제에서는setTimeout()안에서resolve("작업 완료")를 호출한다.
이 말은 “2초 뒤에 이Promise는 성공 상태가 되고, 성공 결과로"작업 완료"를 전달한다”는 뜻이다.
여기서 주의할 점이 있다.
resolve("작업 완료")자체가 바로 화면이나Console에 출력하는 코드는 아니다.
resolve()는 결과를 다음 단계로 넘기는 역할을 한다.
그 결과를 실제로 받아서 출력하는 코드는 아래의then()이다.
resolve()는 결과를 직접 출력하는 코드가 아니라,then()으로 전달할 성공 결과를 넘기는 코드이다.
then은 Promise 성공 결과를 받아서 처리한다
// exam26_1.html work() .then((result) => { // resolve("작업 완료")에서 전달한 값이 result로 들어온다. console.log(result); })
then()은Promise가 성공했을 때 실행되는 메서드이다.
메서드는 객체에 붙어서 실행되는 함수라고 이해하면 된다.
work()는Promise를 반환한다.
그래서 그 뒤에.then(...)을 붙일 수 있다.
resolve("작업 완료")가 실행되면"작업 완료"라는 값이then()의result매개변수로 전달된다.
매개변수는 함수가 외부에서 값을 받기 위해 열어 둔 자리이다.
그래서console.log(result)는 결국"작업 완료"를 출력한다.
이 흐름을 한 줄로 보면 아래와 같다.
resolve("작업 완료")→then((result) => ...)→console.log(result)
then()은resolve()가 전달한 성공 결과를 받아서 다음 작업을 실행하는 부분이다.
finally는 마지막에 실행된다
// exam26_1.html work() .then((result) => { console.log(result); // 성공 결과 출력 }).finally(() => { console.log("다음 작업"); // 마지막 작업 출력 })
finally()는Promise작업이 끝난 뒤 마지막에 실행되는 메서드이다.
성공하든 실패하든 공통으로 실행해야 하는 마무리 작업을 넣을 때 사용한다.
이번 예제에서는Promise가 성공한다.
그래서 먼저then()이 실행되고"작업 완료"가 출력된다.
그다음finally()가 실행되어"다음 작업"이 출력된다.
여기서 중요한 점은"다음 작업"이work()함수 바로 아래에 일반 코드로 작성된 것이 아니라는 점이다.
finally()안에 들어 있기 때문에Promise처리가 끝난 뒤 실행된다.
앞의setTimeout()예제에서는"다음 작업"이 먼저 출력되었다.
하지만 이번 예제에서는"작업 완료"가 출력된 뒤"다음 작업"이 출력된다.
finally()를 사용하면 비동기 작업이 끝난 뒤 마지막에 실행할 코드를 연결할 수 있다.
실제 출력 순서 확인하기
이 예제를 실행하면
Console에는 아래 순서로 출력된다.// 출력결과 // 작업 시작 // 작업 완료 // 다음 작업첫 번째로
"작업 시작"이 출력된다.
Promise객체가 만들어질 때 안쪽 코드가 바로 실행되기 때문이다.
그다음setTimeout()이 2초 뒤resolve("작업 완료")를 실행한다.
resolve()가 전달한"작업 완료"는then()의result로 들어간다.
그래서"작업 완료"가 두 번째로 출력된다.
마지막으로finally()가 실행된다.
그래서"다음 작업"이 세 번째로 출력된다.
출력 순서는작업 시작→작업 완료→다음 작업이다.
앞의setTimeout()예제와 달리,Promise를 사용해서"다음 작업"을 비동기 작업 이후로 연결했기 때문이다.
결과물 뽑기
이 예제의 결과물은 브라우저 화면보다 개발자 도구
Console에서 확인하는 것이 중요하다.
브라우저 화면에는 제목과 설명 문장만 보이고, 실제 실행 순서는Console출력으로 확인해야 한다.
먼저exam26_1.html을 브라우저에서 실행한다.
그다음 개발자 도구를 열고Console탭을 확인한다.
그러면 아래 순서로 로그가 출력된다.// 출력결과 // 작업 시작 // 작업 완료 // 다음 작업
Promise를 사용하면 비동기 작업이 끝난 뒤then()에서 결과를 받고, 마지막에finally()로 다음 작업을 이어서 처리할 수 있다.
이 결과에서 가장 중요한 부분은"다음 작업"의 위치이다.
앞의setTimeout()예제에서는"다음 작업"이"작업 완료"보다 먼저 출력되었다.
하지만 이번 예제에서는"작업 완료"가 출력된 뒤"다음 작업"이 출력된다.
이 차이는"다음 작업"을Promise의finally()안에 넣었기 때문에 생긴다.
즉,"다음 작업"이 비동기 작업이 끝난 뒤 실행되도록 연결된 것이다.
이 예제의 최종 결과물은Console에서작업 시작→작업 완료→다음 작업순서가 보이는 화면이다.
setTimeout만 사용한 예제와 Promise를 사용한 예제 비교하기
앞의 예제와 이번 예제는 둘 다
setTimeout()을 사용한다.
하지만"다음 작업"이 실행되는 위치가 다르다.
앞의 예제에서는"다음 작업"이setTimeout()아래에 일반 코드로 작성되어 있었다.
그래서setTimeout()이 2초 뒤"작업 완료"를 실행하기 전에"다음 작업"이 먼저 출력되었다.
이번 예제에서는"다음 작업"이finally()안에 작성되어 있다.
그래서Promise작업이 끝난 뒤 마지막에 출력된다.
차이를 정리하면 아래처럼 볼 수 있다.
setTimeout()만 사용한 예제는작업 시작→다음 작업→작업 완료순서로 출력된다.Promise를 사용한 예제는작업 시작→작업 완료→다음 작업순서로 출력된다.Promise는 비동기 작업의 결과를 받은 뒤 다음 작업을 연결할 수 있다.finally()안의 코드는Promise처리가 끝난 뒤 실행된다.
Promise를 사용하면 비동기 작업이 끝난 시점을 기준으로 다음 작업을 이어서 실행할 수 있다.
핵심 정리
이 예제의 핵심은
Promise로 비동기 작업의 이후 흐름을 제어하는 것이다.
setTimeout()은"작업 완료"를 2초 뒤 실행하도록 만든다.
하지만Promise를 사용하면 그 작업이 끝난 뒤 어떤 코드를 실행할지 연결할 수 있다.
이 흐름은 아래처럼 정리할 수 있다.
work()함수는Promise객체를 반환한다.Promise안의"작업 시작"은 바로 출력된다.setTimeout()은 2초 뒤resolve("작업 완료")를 실행한다.resolve()는 성공 결과를then()으로 전달한다.then()은 전달받은"작업 완료"를 출력한다.finally()는Promise처리가 끝난 뒤"다음 작업"을 출력한다.- 최종 출력 순서는
작업 시작→작업 완료→다음 작업이다.- 브라우저 화면보다 개발자 도구
Console에서 실행 순서를 확인해야 한다.따라서 이 예제가 알려 주는 핵심은 이것이다.
Promise는 비동기 작업이 끝난 뒤 실행할 코드를then()과finally()로 연결해, 작업 순서를 더 깔끔하게 제어할 수 있게 해 준다.
다음 예제에서는 같은Promise흐름을async/await로 더 읽기 쉽게 작성하는 방법을 확인한다.
이 예제는
async/await를 사용해서Promise결과를 더 읽기 쉬운 순서로 처리하는 흐름을 보여 준다.
앞의 예제에서는Promise결과를then()으로 받았다.
이번 예제에서는then()대신await를 사용해서Promise가 끝날 때까지 기다린 뒤 결과를 받는다.
async/await는 비동기 코드를 순서대로 실행되는 코드처럼 읽을 수 있게 해 주는 문법이다.
실제로 비동기 작업 자체가 동기 코드로 바뀌는 것은 아니다.
다만Promise결과를 처리하는 코드가 위에서 아래로 읽히기 때문에 흐름을 이해하기 쉬워진다.
이 예제의 핵심은await가Promise작업이 끝날 때까지 기다렸다가,resolve()로 전달된 값을 결과로 받는다는 점이다.
예제 전체 코드
// exam26_2.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> </head> <body> <h1>Promise 객체 - 나중에 결과를 줄께!!</h1> <hr> <h2>async/await 는 비동기 코드를 순서대로 실행되는 것처럼 보이게 해주는 문법적 설탕이다.</h2> <h3>work() 를 await 으로 호출하면 Promise 작업이 끝날 때까지 기다리며, resolve() 호출시 전달되는 값을 결과로 받게 된다.</h3> <h3>await 는 async 로 선언된 함수 안에서만 사용할 수 있다.</h3> <script> function work() { // Promise 객체를 만들어 반환한다. return new Promise((resolve, reject) => { // Promise가 만들어지면 바로 실행되는 코드이다. console.log("작업 시작"); // 2초 뒤에 성공 결과를 전달한다. setTimeout(() => { resolve("작업 완료"); }, 2000); }); } // await를 사용하기 위해 async 함수로 선언한다. async function run() { // work()가 반환한 Promise가 끝날 때까지 기다린다. const result = await work(); // resolve()가 전달한 결과를 출력한다. console.log(result); // Promise 결과를 받은 뒤 다음 작업을 실행한다. console.log("다음 작업"); } // run 함수를 호출한다. run(); </script> </body> </html>이 코드는 크게 세 부분으로 나누어 볼 수 있다.
첫 번째는work()함수가Promise객체를 만들어 반환하는 부분이다.
두 번째는run()함수를async함수로 선언하는 부분이다.
세 번째는run()함수 안에서await work()로Promise결과를 기다리는 부분이다.
앞의 예제에서는work()뒤에.then()을 붙여서"작업 완료"를 출력했다.
이번 예제에서는.then()을 쓰지 않는다.
대신const result = await work()로Promise가 완료될 때까지 기다린 뒤,resolve()가 전달한 값을result에 저장한다.
async/await를 사용하면Promise결과를 기다리는 흐름을 위에서 아래로 읽을 수 있다.
async await란 무엇인가
async/await는Promise를 더 읽기 쉽게 다루기 위한 문법이다.
Promise자체를 없애는 문법이 아니다.
내부적으로는 여전히Promise를 사용하지만, 코드를 작성하고 읽는 방식이 달라진다.
앞의 예제에서는then()을 사용했다.
then()방식은Promise가 성공했을 때 실행할 코드를 뒤에 연결하는 방식이다.
이 방식도 맞지만, 연결되는 코드가 많아지면 흐름이 길어질 수 있다.
async/await를 사용하면 비동기 작업을 기다리는 코드를 일반 순서 코드처럼 작성할 수 있다.
그래서work()를 실행하고, 결과를 받고, 그 결과를 출력하고, 다음 작업을 실행하는 흐름이 위에서 아래로 보인다.
여기서async는 함수 앞에 붙는다.
await는async로 선언된 함수 안에서만 사용할 수 있다.
정리하면 아래처럼 볼 수 있다.
async는 이 함수 안에서await를 사용할 수 있게 한다.await는Promise가 끝날 때까지 기다린다.await는resolve()로 전달된 값을 결과로 받는다.async/await는then()보다 코드 흐름을 읽기 쉽게 만들 수 있다.
async/await는 비동기 작업을 동기 작업으로 바꾸는 문법이 아니라,Promise를 더 읽기 쉽게 다루는 문법이다.
work 함수는 Promise를 반환한다
// exam26_2.html function work() { // Promise 객체를 만들어 반환한다. return new Promise((resolve, reject) => { console.log("작업 시작"); // Promise가 만들어질 때 바로 실행된다. setTimeout(() => { resolve("작업 완료"); // 2초 뒤 성공 결과를 전달한다. }, 2000); }); }
work()함수의 구조는 앞의Promise예제와 같다.
이 함수는Promise객체를 만들어 반환한다.
Promise안에서는"작업 시작"이 바로 출력된다.
그다음setTimeout()이 2초 뒤resolve("작업 완료")를 실행하도록 예약한다.
여기서도resolve("작업 완료")는 직접 출력 코드가 아니다.
resolve()는 성공 결과를 다음 단계로 전달한다.
이번 예제에서는 그 다음 단계가then()이 아니라await이다.
work()는 2초 뒤"작업 완료"라는 성공 결과를 전달하는Promise를 반환한다.
async 함수 안에서 await를 사용할 수 있다
// exam26_2.html async function run() { const result = await work(); // Promise 완료를 기다린다. console.log(result); // 성공 결과를 출력한다. console.log("다음 작업"); // 결과 출력 뒤 실행된다. }
run()함수 앞에는async가 붙어 있다.
async는 이 함수 안에서await를 사용할 수 있게 해 주는 표시이다.
await는 아무 곳에서나 사용할 수 없다.
이 예제의 화면 설명처럼await는async로 선언된 함수 안에서만 사용할 수 있다.
그래서await work()는run()함수 안에 작성되어 있다.
const result = await work()는work()가 반환한Promise가 끝날 때까지 기다린다는 뜻이다.
그리고Promise가 성공하면resolve()로 전달된"작업 완료"가result에 들어간다.
그다음console.log(result)가 실행된다.
그래서"작업 완료"가 출력된다.
마지막으로console.log("다음 작업")이 실행된다.
await는Promise가 끝난 뒤 결과를 받아야 하는 위치에서 사용한다.
await는 Promise 결과를 기다린다
// exam26_2.html const result = await work(); console.log(result); console.log("다음 작업");
await work()에서work()는Promise를 반환한다.
await는 이Promise가 처리될 때까지 기다린다.
이 기다림은run()함수 안에서의 흐름을 기다리게 만든다.
즉,const result = await work()다음 줄인console.log(result)는Promise가 끝난 뒤 실행된다.
이 예제에서는 2초 뒤resolve("작업 완료")가 실행된다.
그 결과"작업 완료"가result에 들어간다.
그래서console.log(result)는"작업 완료"를 출력한다.
그 다음에"다음 작업"이 출력된다.
즉,"다음 작업"은Promise결과를 받은 뒤 실행된다.
await를 사용하면Promise가 완료된 뒤 다음 줄의 코드를 실행하도록 흐름을 만들 수 있다.
then 방식과 async await 방식 비교하기
앞의 예제에서는
then()으로Promise결과를 받았다.
이번 예제에서는await로 같은 결과를 받는다.
두 방식은 결과적으로 같은 흐름을 만들 수 있다.
다만 코드 모양이 다르다.
then()방식은 아래처럼 성공했을 때 실행할 코드를 뒤에 연결한다.// then 방식 예시 work() .then((result) => { console.log(result); }).finally(() => { console.log("다음 작업"); })
async/await방식은 아래처럼 기다린 결과를 변수에 담고, 다음 줄에서 처리한다.// async await 방식 예시 async function run() { const result = await work(); console.log(result); console.log("다음 작업"); }
then()방식은Promise뒤에 처리 코드를 연결하는 느낌이다.
async/await방식은 작업 결과를 기다렸다가 다음 줄에서 처리하는 느낌이다.
그래서 초보자 입장에서는async/await방식이 더 직관적으로 보일 수 있다.
작업 실행→결과 받기→결과 출력→다음 작업흐름이 위에서 아래로 보이기 때문이다.
async/await는then()과 다른 결과를 만드는 문법이 아니라, 같은Promise흐름을 더 읽기 쉬운 형태로 작성하는 문법이다.
실제 출력 순서 확인하기
이 예제를 실행하면
Console에는 아래 순서로 출력된다.// 출력결과 // 작업 시작 // 작업 완료 // 다음 작업첫 번째로
"작업 시작"이 출력된다.
work()가 호출되고,Promise안의 코드가 바로 실행되기 때문이다.
그다음setTimeout()이 2초 뒤resolve("작업 완료")를 실행한다.
await work()는 이 결과가 올 때까지 기다린다.
resolve()가 전달한"작업 완료"는result에 저장된다.
그래서"작업 완료"가 두 번째로 출력된다.
마지막으로"다음 작업"이 출력된다.
이 코드는await work()다음 줄에 있기 때문에,Promise결과를 받은 뒤 실행된다.
출력 순서는작업 시작→작업 완료→다음 작업이다.
await가Promise완료를 기다린 뒤 다음 줄을 실행하기 때문이다.
결과물 뽑기
이 예제의 결과물은 브라우저 화면보다 개발자 도구
Console에서 확인하는 것이 중요하다.
브라우저 화면에는 제목과 설명 문장만 보이고, 실제 실행 순서는Console출력으로 확인해야 한다.
먼저exam26_2.html을 브라우저에서 실행한다.
그다음 개발자 도구를 열고Console탭을 확인한다.
그러면 아래 순서로 로그가 출력된다.// 출력결과 // 작업 시작 // 작업 완료 // 다음 작업
await work()가Promise완료를 기다리기 때문에,작업 완료가 출력된 뒤다음 작업이 실행된다.
이 결과에서 중요한 부분은"다음 작업"이"작업 완료"뒤에 출력된다는 점이다.
앞의setTimeout()예제에서는"다음 작업"이 먼저 출력되었다.
하지만 이번 예제에서는await가Promise완료를 기다리기 때문에"작업 완료"가 먼저 출력된다.
이 결과를 통해async/await가Promise결과를 기다린 뒤 다음 작업을 이어서 실행하게 해 주는 문법이라는 점을 확인할 수 있다.
이 예제의 최종 결과물은Console에서작업 시작→작업 완료→다음 작업순서가 보이는 화면이다.
핵심 정리
이 예제의 핵심은
async/await로Promise결과를 기다리는 흐름이다.
work()함수는Promise를 반환하고,setTimeout()은 2초 뒤resolve("작업 완료")를 실행한다.
이 흐름은 아래처럼 정리할 수 있다.
work()함수는Promise객체를 반환한다.Promise안의"작업 시작"은 바로 출력된다.setTimeout()은 2초 뒤resolve("작업 완료")를 실행한다.run()함수는async로 선언되어 있다.await는async함수 안에서만 사용할 수 있다.await work()는work()가 반환한Promise가 끝날 때까지 기다린다.resolve()가 전달한"작업 완료"는result에 저장된다.console.log(result)로"작업 완료"가 출력된다.- 그다음
"다음 작업"이 출력된다.- 최종 출력 순서는
작업 시작→작업 완료→다음 작업이다.따라서 이 예제가 알려 주는 핵심은 이것이다.
async/await는Promise결과를 기다렸다가 다음 코드를 실행하는 흐름을 위에서 아래로 읽기 쉽게 만들어 준다.
다음 예제에서는Promise를 실제 커피 주문 예제에 적용해서 여러 비동기 작업이 어떻게 처리되는지 확인한다.
이 예제는
Promise를 실제 주문 처리 흐름에 적용해서, 비동기 작업이 여러 개 실행될 때 결과가 어떻게 처리되는지 보여 준다.
앞의 예제에서는Promise하나가 끝난 뒤then()과finally()가 실행되는 흐름을 확인했다.
이번 예제에서는 커피 주문 세 개를 각각Promise로 처리한다.
각 주문은orderCoffee()함수를 통해 실행된다.
orderCoffee()는 주문이 들어오면 먼저 주문 로그를 출력하고,random()으로 대기 시간을 정한다.
그다음 정해진 시간이 지나면resolve()로 성공 결과를 전달한다.
resolve()로 전달된 결과는then()에서 처리한다.
실패 상황은catch()에서 처리할 수 있고, 성공이나 실패와 관계없이 마지막 마무리는finally()에서 처리한다.
이 예제의 핵심은 여러 개의Promise작업을 기다리지 않고 연속으로 등록하면, 완료 순서는 코드 작성 순서가 아니라 각 작업이 끝나는 시간에 따라 달라질 수 있다는 점이다.
예제 전체 코드
// random.js export function random() { // 3부터 13 사이의 랜덤 숫자를 만든다. const randomValue = Math.floor(Math.random() * 11) + 3 // 이번 주문의 대기 시간이 몇 초인지 출력한다. console.log(`randomValue=${randomValue} 초...`); // 랜덤 값을 반환한다. return randomValue; }// coffee.js import { random } from "./random.js"; // 커피 주문을 처리하는 Promise 생성 함수이다. export function orderCoffee(menu) { return new Promise((resolve, reject) => { // 주문이 들어온 시점을 출력한다. console.log(`[주문] ${menu} 주문이 들어왔습니다.`); // random()으로 정한 시간 뒤 커피가 준비된다고 가정한다. setTimeout(() => { const isAvailable = true; // 재료가 있다고 가정한다. if (isAvailable) { // 성공하면 준비 완료 메시지를 전달한다. resolve(`${menu}가 준비되었습니다! ☕`); } else { // 실패하면 실패 이유를 전달한다. reject("죄송합니다. 재료가 소진되었습니다."); } }, random() * 1000); }); }// exam27.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> </head> <body> <h1>Promise 객체 테스트(1)</h1> <hr> <script type="module"> import { orderCoffee } from "./src/coffee.js"; // 주문 실행 시작을 출력한다. console.log("--- 가게 문을 엽니다 ---"); // 첫 번째 커피 주문을 Promise로 처리한다. orderCoffee("둘리의 아메리카노") .then((message) => { // 성공 시 실행되는 곳이다. console.log(`[성공] ${message}`); }) .catch((error) => { // 실패 시 실행되는 곳이다. console.log(`[실패] ${error}`); }) .finally(() => { // 성공 또는 실패 후 마지막에 실행된다. console.log("--- 둘리의 아메리카노 다음 주문을 기다립니다 ---"); }); // 두 번째 커피 주문을 Promise로 처리한다. orderCoffee("도우너의 아메리카노") .then((message) => { // 성공 시 실행되는 곳이다. console.log(`[성공] ${message}`); }) .catch((error) => { // 실패 시 실행되는 곳이다. console.log(`[실패] ${error}`); }) .finally(() => { // 성공 또는 실패 후 마지막에 실행된다. console.log("--- 도우너의 아메리카노 다음 주문을 기다립니다 ---"); }); // 세 번째 커피 주문을 Promise로 처리한다. orderCoffee("또치의 아메리카노") .then((message) => { // 성공 시 실행되는 곳이다. console.log(`[성공] ${message}`); }) .catch((error) => { // 실패 시 실행되는 곳이다. console.log(`[실패] ${error}`); }) .finally(() => { // 성공 또는 실패 후 마지막에 실행된다. console.log("--- 또치의 아메리카노 다음 주문을 기다립니다 ---"); }); </script> </body> </html>이 코드는 크게 세 파일로 나누어 볼 수 있다.
첫 번째random.js는 주문마다 사용할 랜덤 대기 시간을 만드는 모듈이다.
두 번째coffee.js는 커피 주문을Promise로 처리하는 모듈이다.
세 번째exam27.html은orderCoffee()를 호출해서 실제 주문 결과를 확인하는 실행 파일이다.
여기서module은 코드를 기능별 파일로 나누어 사용하는 방식이다.
random.js는 랜덤 값을 만드는 역할만 맡고,coffee.js는 커피 주문을 처리하는 역할만 맡는다.
exam27.html은 이 기능을 가져와서 실행한다.
이 예제는Promise와module을 함께 사용해서, 비동기 주문 처리 기능을 파일별 역할로 나누어 실행하는 흐름을 보여 준다.
이 예제에서 확인할 핵심
random.js는 주문마다 사용할 랜덤 대기 시간을 만든다.coffee.js는 커피 주문을 처리하는Promise를 반환한다.exam27.html은orderCoffee()를 세 번 호출한다.orderCoffee()를 호출하면 주문 로그가 바로 출력된다.random()은setTimeout()의 대기 시간을 정할 때 바로 실행된다.setTimeout()은 랜덤 시간 뒤 커피 준비 완료 처리를 실행한다.- 커피 준비가 성공하면
resolve()가 실행된다.resolve()가 전달한 메시지는then()에서 받는다.- 실패하면
reject()가 실행되고catch()에서 받을 수 있다.finally()는 성공과 실패에 관계없이 마지막에 실행된다.- 여러 주문은 서로를 기다리지 않고 등록되므로 완료 순서는 랜덤 시간에 따라 달라질 수 있다.
이 예제는 여러 개의 비동기 작업을 각각
Promise로 실행하고, 각 작업의 성공, 실패, 마무리 흐름을 체이닝으로 처리하는 예제이다.
random.js는 랜덤 대기 시간을 만든다
// random.js export function random() { const randomValue = Math.floor(Math.random() * 11) + 3 console.log(`randomValue=${randomValue} 초...`); return randomValue; }
random.js에는random()함수가 들어 있다.
이 함수는 커피가 준비되는 데 걸리는 시간을 랜덤하게 만들기 위해 사용한다.
Math.random()은0이상1미만의 랜덤 숫자를 만든다.
여기에11을 곱하고Math.floor()로 소수점을 버린 뒤,3을 더한다.
그래서3부터13사이의 정수가 만들어진다.
이 값은 초 단위처럼 사용된다.
예를 들어randomValue가5라면 커피가 약 5초 뒤 준비되는 것처럼 처리된다.
실제 대기 시간은coffee.js에서random() * 1000으로 계산된다.
console.log()로randomValue=... 초...를 출력하는 이유는 각 주문의 대기 시간이 몇 초로 정해졌는지 확인하기 위해서이다.
이 로그는 커피가 완료되었다는 뜻이 아니라, 이번 주문의 대기 시간이 정해졌다는 뜻이다.
random.js는 각 커피 주문의 완료 시간이 서로 다르게 나오도록 랜덤 값을 만들어 주는 역할을 한다.
coffee.js는 주문을 Promise로 처리한다
// coffee.js import { random } from "./random.js"; export function orderCoffee(menu) { return new Promise((resolve, reject) => { console.log(`[주문] ${menu} 주문이 들어왔습니다.`); setTimeout(() => { const isAvailable = true; if (isAvailable) { resolve(`${menu}가 준비되었습니다! ☕`); } else { reject("죄송합니다. 재료가 소진되었습니다."); } }, random() * 1000); }); }
coffee.js는 커피 주문을 처리하는 파일이다.
이 파일에서는random.js에서 만든random()함수를 가져와 사용한다.
import { random } from "./random.js"는 다른 파일에서 내보낸random()함수를 가져오는 코드이다.
가져온random()함수는setTimeout()의 대기 시간을 정할 때 사용된다.
orderCoffee(menu)는 커피 메뉴 이름을 받아 주문을 처리한다.
여기서menu는 주문 이름이 들어오는 매개변수이다.
예를 들어"둘리의 아메리카노"를 전달하면, 해당 문자열이menu에 들어간다.
orderCoffee()는Promise를 반환한다.
그래서 이 함수를 호출한 뒤.then(),.catch(),.finally()를 붙여 결과를 이어서 처리할 수 있다.
orderCoffee()는 커피 주문이라는 비동기 작업을Promise객체로 감싸서 반환하는 함수이다.
주문이 들어오면 먼저 주문 로그가 출력된다
// coffee.js console.log(`[주문] ${menu} 주문이 들어왔습니다.`);
orderCoffee()가 호출되면Promise안의 코드가 실행된다.
가장 먼저 주문 로그가 출력된다.
예를 들어orderCoffee("둘리의 아메리카노")가 실행되면 아래와 같은 로그가 출력된다.// 출력 예시 // [주문] 둘리의 아메리카노 주문이 들어왔습니다.이 로그는 커피가 준비된 결과가 아니다.
단지 주문이 들어왔다는 것을 확인하는 로그이다.
커피 준비 완료 메시지는 나중에 출력된다.
그 이유는 준비 완료 처리가setTimeout()안에 들어 있기 때문이다.
주문 로그는 바로 출력되지만, 준비 완료 메시지는 랜덤 시간이 지난 뒤 출력된다.
setTimeout과 random으로 주문별 완료 시간을 다르게 만든다
// coffee.js setTimeout(() => { const isAvailable = true; if (isAvailable) { resolve(`${menu}가 준비되었습니다! ☕`); } else { reject("죄송합니다. 재료가 소진되었습니다."); } }, random() * 1000);
setTimeout()은 일정 시간이 지난 뒤 코드를 실행한다.
여기서는 두 번째 값으로random() * 1000을 전달한다.
이때random()은 커피가 완료될 때 실행되는 함수가 아니다.
setTimeout()에 전달할 대기 시간을 계산하기 위해 먼저 실행된다.
그래서 주문이 들어온 직후randomValue=... 초...로그가 출력된다.
random()은3부터13사이의 숫자를 반환한다.
여기에1000을 곱하면 밀리초 단위 시간이 된다.
예를 들어random()이5를 반환하면5000밀리초가 되고, 약 5초 뒤 코드가 실행된다.
각 주문마다random()결과가 다를 수 있다.
그래서 먼저 주문한 커피가 항상 먼저 준비되는 것은 아니다.
나중에 주문한 커피라도 랜덤 시간이 더 짧으면 먼저 완료될 수 있다.
여러 비동기 작업을 기다리지 않고 등록하면 완료 순서는 시작 순서가 아니라 실제 작업이 끝나는 시간에 따라 달라진다.
resolve와 reject로 성공과 실패를 나눈다
// coffee.js const isAvailable = true; if (isAvailable) { resolve(`${menu}가 준비되었습니다! ☕`); } else { reject("죄송합니다. 재료가 소진되었습니다."); }
Promise에서는 성공과 실패 흐름을 나누어 처리할 수 있다.
성공했을 때는resolve()를 호출한다.
실패했을 때는reject()를 호출한다.
이 예제에서는isAvailable이true로 고정되어 있다.
그래서 항상resolve()가 실행된다.
즉, 현재 코드 기준으로는 커피 주문이 모두 성공한다.
resolve()에는 성공 메시지가 들어간다.
예를 들어menu가"둘리의 아메리카노"라면 아래 문자열이 성공 결과로 전달된다.// resolve로 전달되는 값 예시 // 둘리의 아메리카노가 준비되었습니다! ☕반대로
isAvailable이false라면reject()가 실행된다.
이 경우 실패 이유가catch()로 전달된다.
현재 코드에서는isAvailable이true이므로 모든 주문이 성공 흐름인resolve()로 처리된다.
exam27.html은 커피 주문을 세 번 실행한다
// exam27.html console.log("--- 가게 문을 엽니다 ---"); orderCoffee("둘리의 아메리카노") .then((message) => { console.log(`[성공] ${message}`); }) .catch((error) => { console.log(`[실패] ${error}`); }) .finally(() => { console.log("--- 둘리의 아메리카노 다음 주문을 기다립니다 ---"); }); orderCoffee("도우너의 아메리카노") .then((message) => { console.log(`[성공] ${message}`); }) .catch((error) => { console.log(`[실패] ${error}`); }) .finally(() => { console.log("--- 도우너의 아메리카노 다음 주문을 기다립니다 ---"); }); orderCoffee("또치의 아메리카노") .then((message) => { console.log(`[성공] ${message}`); }) .catch((error) => { console.log(`[실패] ${error}`); }) .finally(() => { console.log("--- 또치의 아메리카노 다음 주문을 기다립니다 ---"); });
exam27.html에서는orderCoffee()를 세 번 호출한다.
각 호출은 서로 다른 커피 주문을 의미한다.
첫 번째는 둘리의 아메리카노 주문이다.
두 번째는 도우너의 아메리카노 주문이다.
세 번째는 또치의 아메리카노 주문이다.
중요한 점은 첫 번째 주문이 끝난 뒤 두 번째 주문을 시작하는 구조가 아니라는 점이다.
첫 번째orderCoffee()를 호출한 뒤 그 주문의 완료를 기다리지 않고, 바로 두 번째orderCoffee()를 호출한다.
두 번째 주문도 완료를 기다리지 않고, 바로 세 번째 주문을 호출한다.
그래서 주문 시작 로그와randomValue로그는 코드 흐름에 따라 빠르게 출력된다.
하지만 성공 메시지는 각 주문의 대기 시간이 끝난 뒤 출력된다.
세 주문은 각각 독립적인Promise로 등록되기 때문에, 완료 순서는 매번 달라질 수 있다.
then catch finally로 주문 결과를 처리한다
// exam27.html orderCoffee("둘리의 아메리카노") .then((message) => { console.log(`[성공] ${message}`); }) .catch((error) => { console.log(`[실패] ${error}`); }) .finally(() => { console.log("--- 둘리의 아메리카노 다음 주문을 기다립니다 ---"); });
orderCoffee()는Promise를 반환한다.
그래서 뒤에then(),catch(),finally()를 이어 붙일 수 있다.
이렇게Promise뒤에 메서드를 이어 붙이는 흐름을Promise체이닝이라고 볼 수 있다.
then()은 주문이 성공했을 때 실행된다.
resolve()가 전달한 성공 메시지가message로 들어온다.
그래서[성공] ...형태의 로그가 출력된다.
catch()는 주문이 실패했을 때 실행된다.
reject()가 전달한 실패 메시지가error로 들어온다.
현재 코드에서는isAvailable이true이므로 실제 실행 결과에서는catch()가 실행되지 않는다.
finally()는 성공과 실패에 관계없이 마지막에 실행된다.
현재 코드에서는 성공 메시지가 출력된 뒤 해당 주문의 다음 주문 대기 메시지가 출력된다.
then(),catch(),finally()를 사용하면 비동기 작업의 성공, 실패, 마무리 흐름을 나누어 처리할 수 있다.
실제 출력 순서 확인하기
이 예제를 실행하면
Console에는 주문 시작 로그와 랜덤 시간 로그가 먼저 출력된다.
그 뒤 각 주문의 준비 완료 시간이 되었을 때 성공 메시지와 마무리 메시지가 출력된다.
출력 예시는 아래처럼 볼 수 있다.// 출력결과 예시 // --- 가게 문을 엽니다 --- // [주문] 둘리의 아메리카노 주문이 들어왔습니다. // randomValue=... 초... // [주문] 도우너의 아메리카노 주문이 들어왔습니다. // randomValue=... 초... // [주문] 또치의 아메리카노 주문이 들어왔습니다. // randomValue=... 초... // [성공] ...가 준비되었습니다! ☕ // --- ... 다음 주문을 기다립니다 ---여기서
randomValue는 실행할 때마다 달라질 수 있다.
그래서[성공]메시지가 나오는 순서도 매번 같지 않을 수 있다.
예를 들어 둘리 주문의 랜덤 시간이 가장 짧으면 둘리 성공 메시지가 먼저 출력된다.
반대로 또치 주문의 랜덤 시간이 가장 짧으면 또치 성공 메시지가 먼저 출력될 수 있다.
이 예제의 출력 결과는 랜덤 대기 시간에 따라 매번 달라질 수 있으므로, 고정된 성공 메시지 순서를 외우면 안 된다.
결과물 뽑기
이 예제의 결과물은 브라우저 화면보다 개발자 도구
Console에서 확인하는 것이 중요하다.
브라우저 화면에는 제목만 보이고, 실제 주문 처리 흐름은Console출력으로 확인해야 한다.
먼저exam27.html을 브라우저에서 실행한다.
그다음 개발자 도구를 열고Console탭을 확인한다.
그러면 주문 시작 로그, 랜덤 시간 로그, 성공 메시지, 다음 주문 대기 메시지가 출력된다.// 출력결과 예시 // --- 가게 문을 엽니다 --- // [주문] 둘리의 아메리카노 주문이 들어왔습니다. // randomValue=... 초... // [주문] 도우너의 아메리카노 주문이 들어왔습니다. // randomValue=... 초... // [주문] 또치의 아메리카노 주문이 들어왔습니다. // randomValue=... 초... // [성공] ...가 준비되었습니다! ☕ // --- ... 다음 주문을 기다립니다 ---
각 주문은
Promise로 처리되고, 완료된 주문부터then()과finally()흐름이 실행된다.
이 결과에서 중요한 부분은 주문 시작 순서와 완료 순서가 반드시 같지 않을 수 있다는 점이다.
세 주문은 서로의 완료를 기다리지 않고 등록되지만, 각 주문의 대기 시간은random()으로 정해진다.
그래서 성공 메시지는 랜덤 시간이 짧은 주문부터 출력된다.
또 각 주문이 성공하면 바로 해당 주문의finally()가 실행된다.
그래서[성공] ...메시지 뒤에--- ... 다음 주문을 기다립니다 ---메시지가 이어서 출력된다.
이 예제의 최종 결과물은 세 주문이 각각Promise로 실행되고, 랜덤 대기 시간이 끝난 주문부터 성공 처리와 마무리 처리가 실행되는Console화면이다.
Promise 체이닝 흐름 정리하기
Promise체이닝은Promise뒤에then(),catch(),finally()같은 메서드를 이어 붙여 처리 흐름을 만드는 방식이다.
이 예제에서는 각 커피 주문마다 같은 체이닝 구조를 사용한다.
흐름을 한 줄로 정리하면 아래와 같다.
orderCoffee()호출 →Promise생성 → 주문 로그 출력 → 랜덤 대기 시간 설정 →setTimeout()대기 →resolve()실행 →then()실행 →finally()실행
이 흐름은 주문 하나마다 따로 적용된다.
둘리 주문도 자기만의Promise흐름을 가진다.
도우너 주문도 자기만의Promise흐름을 가진다.
또치 주문도 자기만의Promise흐름을 가진다.
그래서 세 주문은 서로 독립적으로 처리된다.
하나의 주문이 끝날 때까지 다른 주문이 무조건 기다리는 구조가 아니다.
여러Promise를 기다리지 않고 등록하면 각Promise는 독립적으로 진행되고, 먼저 끝난 작업부터 결과 처리가 실행된다.
핵심 정리
이 예제의 핵심은 여러 커피 주문을 각각
Promise로 처리하는 흐름이다.
orderCoffee()는 주문 하나를Promise로 만들고, 랜덤 시간이 지난 뒤 성공 결과를 전달한다.
이 흐름은 아래처럼 정리할 수 있다.
random.js는 주문마다 다른 대기 시간을 만들기 위해 사용한다.coffee.js는orderCoffee()함수를 정의한다.orderCoffee()는Promise객체를 반환한다.exam27.html은orderCoffee()를 세 번 호출한다.- 각 주문은 자기만의
Promise로 실행된다.- 주문 로그는 주문이 들어오면 바로 출력된다.
randomValue로그는 주문의 대기 시간이 정해졌다는 뜻이다.- 준비 완료 메시지는 랜덤 시간이 지난 뒤 출력된다.
resolve()는 성공 메시지를then()으로 전달한다.- 현재 코드에서는
isAvailable이true라서catch()는 실행되지 않는다.finally()는 각 주문의 성공 처리 뒤 마지막에 실행된다.- 여러 주문의 완료 순서는
random()결과에 따라 달라질 수 있다.따라서 이 예제가 알려 주는 핵심은 이것이다.
Promise체이닝을 사용하면 여러 비동기 작업의 성공, 실패, 마무리 흐름을 각각 분리해서 처리할 수 있고, 기다리지 않고 등록된 작업은 먼저 끝난 순서대로 결과가 처리된다.
다음 예제에서는 같은 커피 주문 흐름을async/await로 더 간결하게 작성하는 방법을 확인한다.
이 예제는 앞에서 작성한 커피 주문
Promise흐름을async/await로 바꾸어 처리하는 예제이다.
앞의 예제에서는orderCoffee()뒤에then(),catch(),finally()를 직접 이어 붙였다.
이번 예제에서는 주문 처리 흐름을requestOrder()라는async함수로 묶고, 그 안에서await로Promise결과를 기다린다.
async/await를 사용하면Promise결과를 기다리는 코드를 위에서 아래로 읽기 쉽게 작성할 수 있다.
성공 처리는try, 실패 처리는catch, 마무리 처리는finally로 나누어 볼 수 있다.
다만 이 예제에서 중요한 점이 있다.
requestOrder()안에서는await가 해당 주문 하나의 결과를 기다린다.
하지만requestOrder("둘리의 아메리카노"),requestOrder("도우너의 아메리카노"),requestOrder("또치의 아메리카노")세 호출은 서로를 기다리지 않고 연속으로 실행된다.
이 예제의 핵심은async/await를 사용하면 각 주문의 성공, 실패, 마무리 흐름을 읽기 쉽게 만들 수 있지만, 여러async함수를 기다리지 않고 호출하면 완료 순서는 여전히 작업이 끝나는 시간에 따라 달라질 수 있다는 점이다.
예제 전체 코드
// random.js export function random() { // 3부터 13 사이의 랜덤 숫자를 만든다. const randomValue = Math.floor(Math.random() * 11) + 3 // 이번 주문의 대기 시간이 몇 초인지 출력한다. console.log(`randomValue=${randomValue} 초...`); // 랜덤 값을 반환한다. return randomValue; }// coffee.js import { random } from "./random.js"; // 커피 주문을 처리하는 Promise 생성 함수이다. export function orderCoffee(menu) { return new Promise((resolve, reject) => { // 주문이 들어온 시점을 출력한다. console.log(`[주문] ${menu} 주문이 들어왔습니다.`); // random()으로 정한 시간 뒤 커피가 준비된다고 가정한다. setTimeout(() => { const isAvailable = true; // 재료가 있다고 가정한다. if (isAvailable) { // 성공하면 준비 완료 메시지를 전달한다. resolve(`${menu}가 준비되었습니다! ☕`); } else { // 실패하면 실패 이유를 전달한다. reject("죄송합니다. 재료가 소진되었습니다."); } }, random() * 1000); }); }// exam27_1.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> </head> <body> <h1>Promise 객체 테스트(2)</h1> <hr> <h2>async/await 는 비동기 코드를 순서대로 실행되는 것처럼 보이게 해주는 문법이다.</h2> <h3>Promise 의 then() 을 더 읽기 쉽고 편리하게 표현한 문법이다.</h3> <pre style="color:blue"> fetch("/user") .then(response => response.json()) .then(data => console.log(data)); ---> const response = await fetch("/user"); const data = await response.json(); console.log(data); </pre> <hr> <script type="module"> import { orderCoffee } from "./src/coffee.js"; // 주문 실행 시작을 출력한다. console.log("--- 가게 문을 엽니다 ---"); // 주문 하나를 처리하는 async 함수이다. async function requestOrder(name) { try { // orderCoffee()가 반환한 Promise 결과를 기다린다. const message = await orderCoffee(name); // 성공 결과를 출력한다. console.log(`[성공] ${message}`); } catch (error) { // 실패 결과를 출력한다. console.log(`[실패] ${error}`); } finally { // 성공 또는 실패와 관계없이 마지막에 실행된다. console.log(`--- ${name} 다음 주문을 기다립니다 ---`); } } // 주문 함수를 세 번 호출한다. requestOrder("둘리의 아메리카노"); requestOrder("도우너의 아메리카노"); requestOrder("또치의 아메리카노"); </script> </body> </html>이 코드는 크게 세 파일로 나누어 볼 수 있다.
첫 번째random.js는 주문마다 사용할 랜덤 대기 시간을 만든다.
두 번째coffee.js는 커피 주문을Promise로 처리하는orderCoffee()함수를 만든다.
세 번째exam27_1.html은requestOrder()라는async함수로 주문을 실행하고 결과를 확인한다.
앞의 예제와 비교하면random.js와coffee.js의 역할은 그대로이다.
달라진 부분은exam27.html에서 사용했던then(),catch(),finally()체이닝을exam27_1.html에서는try/catch/finally와await로 바꾼 점이다.
이 예제는 같은Promise기반 주문 처리 기능을async/await문법으로 더 읽기 쉽게 작성하는 흐름을 보여 준다.
이 예제에서 확인할 핵심
orderCoffee()는 여전히Promise를 반환한다.requestOrder()는 주문 하나를 처리하는async함수이다.await orderCoffee(name)은 해당 주문의Promise결과를 기다린다.resolve()로 전달된 성공 메시지는message변수에 들어간다.- 성공하면
try블록 안에서[성공]메시지를 출력한다.- 실패하면
catch블록에서[실패]메시지를 출력한다.finally블록은 성공과 실패에 관계없이 마지막에 실행된다.requestOrder()세 번은 서로를 기다리지 않고 연속으로 호출된다.- 그래서 주문 완료 순서는
random()으로 정해진 대기 시간에 따라 달라질 수 있다.이 예제는
Promise체이닝으로 처리하던 성공, 실패, 마무리 흐름을async/await와try/catch/finally로 바꾸어 작성하는 예제이다.
async await는 Promise를 더 읽기 쉽게 다루는 문법이다
async/await는Promise를 없애는 문법이 아니다.
Promise를 더 읽기 쉬운 코드 모양으로 다루기 위한 문법이다.
앞의 예제에서는 아래처럼Promise뒤에then(),catch(),finally()를 이어 붙였다.// then 방식 예시 orderCoffee("둘리의 아메리카노") .then((message) => { console.log(`[성공] ${message}`); }) .catch((error) => { console.log(`[실패] ${error}`); }) .finally(() => { console.log("--- 다음 주문을 기다립니다 ---"); });
then()방식은Promise가 성공했을 때 실행할 코드를 뒤에 연결하는 방식이다.
틀린 방식은 아니지만, 처리할 코드가 많아지면 흐름이 길어 보일 수 있다.
async/await방식은 아래처럼Promise결과를 기다린 뒤 다음 줄에서 처리한다.// async await 방식 예시 async function requestOrder(name) { try { const message = await orderCoffee(name); console.log(`[성공] ${message}`); } catch (error) { console.log(`[실패] ${error}`); } finally { console.log(`--- ${name} 다음 주문을 기다립니다 ---`); } }이 방식은
주문 실행→결과 기다림→성공 출력→마무리 출력흐름이 위에서 아래로 보인다.
그래서 초보자가 실행 흐름을 따라가기 더 쉽다.
async/await는then()과 다른 결과를 만드는 문법이 아니라, 같은Promise흐름을 더 읽기 쉬운 형태로 작성하는 문법이다.
requestOrder는 주문 하나를 처리하는 async 함수이다
// exam27_1.html async function requestOrder(name) { try { const message = await orderCoffee(name); console.log(`[성공] ${message}`); } catch (error) { console.log(`[실패] ${error}`); } finally { console.log(`--- ${name} 다음 주문을 기다립니다 ---`); } }
requestOrder()는 주문 하나를 처리하는 함수이다.
함수 앞에async가 붙어 있으므로, 이 함수 안에서는await를 사용할 수 있다.
name은 주문 이름을 받는 매개변수이다.
예를 들어requestOrder("둘리의 아메리카노")를 호출하면"둘리의 아메리카노"가name에 들어간다.
함수 안에서는try,catch,finally가 사용된다.
try는 정상 실행을 시도하는 구간이다.
catch는 오류나 실패가 발생했을 때 처리하는 구간이다.
finally는 성공과 실패에 관계없이 마지막에 실행되는 구간이다.
requestOrder()는 주문 하나의 성공, 실패, 마무리 흐름을 한 함수 안에 정리한 구조이다.
await orderCoffee는 주문 결과를 기다린다
// exam27_1.html const message = await orderCoffee(name); console.log(`[성공] ${message}`);
orderCoffee(name)은Promise를 반환한다.
앞에서 본coffee.js의orderCoffee()함수가 바로 그 역할을 한다.
await orderCoffee(name)은 이Promise가 끝날 때까지 기다린다.
주문이 성공하면resolve()가 전달한 메시지가message변수에 들어간다.
예를 들어name이"둘리의 아메리카노"이고 주문이 성공하면,message에는 아래와 같은 문자열이 들어갈 수 있다.// message에 들어가는 값 예시 // 둘리의 아메리카노가 준비되었습니다! ☕그다음
console.log([성공] ${message})가 실행된다.
그래서Console에는[성공] 둘리의 아메리카노가 준비되었습니다! ☕처럼 출력된다.
await는orderCoffee()가 반환한Promise결과를 기다렸다가, 성공 결과를 변수에 담아 준다.
try catch finally로 성공 실패 마무리를 나눈다
// exam27_1.html try { const message = await orderCoffee(name); console.log(`[성공] ${message}`); } catch (error) { console.log(`[실패] ${error}`); } finally { console.log(`--- ${name} 다음 주문을 기다립니다 ---`); }
try블록에는 정상적으로 실행하고 싶은 코드가 들어간다.
이 예제에서는await orderCoffee(name)으로 주문 결과를 기다리고, 성공 메시지를 출력한다.
주문이 성공하면resolve()가 실행된다.
그러면message에 성공 메시지가 들어가고,[성공] ...로그가 출력된다.
catch블록은 실패를 처리하는 구간이다.
만약orderCoffee()안에서reject()가 실행되면, 실패 이유가error에 들어간다.
그때[실패] ...로그가 출력된다.
현재coffee.js에서는isAvailable이true로 되어 있다.
그래서 실제 실행 결과에서는reject()가 실행되지 않는다.
즉, 현재 코드 기준으로는catch가 실행되지 않고 성공 흐름만 확인된다.
finally블록은 성공하든 실패하든 마지막에 실행된다.
그래서 각 주문마다 성공 메시지가 출력된 뒤--- ... 다음 주문을 기다립니다 ---메시지가 출력된다.
try/catch/finally를 사용하면async/await코드에서 성공, 실패, 마무리 흐름을 분리해서 처리할 수 있다.
requestOrder 세 번은 서로 기다리지 않고 호출된다
// exam27_1.html requestOrder("둘리의 아메리카노"); requestOrder("도우너의 아메리카노"); requestOrder("또치의 아메리카노");이 부분은 초보자가 헷갈리기 쉽다.
requestOrder()안에는await가 있다.
그래서 각 주문 함수 안에서는 해당 주문의 결과를 기다린다.
하지만 위 코드에서requestOrder()호출 앞에는await가 붙어 있지 않다.
즉, 첫 번째requestOrder()가 완전히 끝날 때까지 기다린 뒤 두 번째를 실행하는 구조가 아니다.
브라우저는 첫 번째requestOrder()를 호출하고, 바로 두 번째requestOrder()를 호출한다.
두 번째도 끝까지 기다리지 않고 바로 세 번째를 호출한다.
그래서 세 주문은 각각 자기Promise를 가지고 진행된다.
완료 순서는random()으로 정해진 대기 시간에 따라 달라진다.
await가 함수 안에 있다고 해서 여러 함수 호출이 자동으로 순서대로 끝나는 것은 아니다.
여러 호출 자체를 순서대로 기다리려면 호출하는 쪽에서도await requestOrder(...)처럼 기다리는 구조가 필요하다.
실제 출력 순서 확인하기
이 예제를 실행하면
Console에는 먼저 가게 문을 여는 로그가 출력된다.
그다음 세 주문의 주문 로그와 랜덤 대기 시간 로그가 출력된다.
출력 예시는 아래처럼 볼 수 있다.// 출력결과 예시 // --- 가게 문을 엽니다 --- // [주문] 둘리의 아메리카노 주문이 들어왔습니다. // randomValue=... 초... // [주문] 도우너의 아메리카노 주문이 들어왔습니다. // randomValue=... 초... // [주문] 또치의 아메리카노 주문이 들어왔습니다. // randomValue=... 초... // [성공] ...가 준비되었습니다! ☕ // --- ... 다음 주문을 기다립니다 ---여기서
randomValue는 주문마다 달라질 수 있다.
그래서[성공]메시지가 출력되는 순서도 매번 같지 않을 수 있다.
async/await를 사용했기 때문에 각requestOrder()함수 안에서는 성공 메시지 뒤에 해당 주문의finally메시지가 이어진다.
하지만 세 주문 전체의 완료 순서는 랜덤 대기 시간에 따라 달라질 수 있다.
이 예제는async/await를 사용해도 여러 비동기 작업을 기다리지 않고 호출하면 먼저 끝난 작업부터 결과가 처리된다는 점을 보여 준다.
결과물 뽑기
이 예제의 결과물은 브라우저 화면보다 개발자 도구
Console에서 확인하는 것이 중요하다.
브라우저 화면에는 제목과 설명 문장만 보이고, 실제 주문 처리 흐름은Console출력으로 확인해야 한다.
먼저exam27_1.html을 브라우저에서 실행한다.
그다음 개발자 도구를 열고Console탭을 확인한다.
그러면 주문 시작 로그, 랜덤 시간 로그, 성공 메시지, 다음 주문 대기 메시지가 출력된다.// 출력결과 예시 // --- 가게 문을 엽니다 --- // [주문] 둘리의 아메리카노 주문이 들어왔습니다. // randomValue=... 초... // [주문] 도우너의 아메리카노 주문이 들어왔습니다. // randomValue=... 초... // [주문] 또치의 아메리카노 주문이 들어왔습니다. // randomValue=... 초... // [성공] ...가 준비되었습니다! ☕ // --- ... 다음 주문을 기다립니다 ---
async/await를 사용하면 각 주문의Promise결과를 기다린 뒤 성공 메시지를 출력하고, 마지막에finally흐름을 실행할 수 있다.
이 결과에서 중요한 부분은 각 주문의 성공 메시지와finally메시지가 한 쌍으로 이어진다는 점이다.
예를 들어 어떤 주문이 먼저 완료되면[성공] ...메시지가 출력되고, 바로 이어서 그 주문의 다음 주문 대기 메시지가 출력된다.
하지만 세 주문의 전체 완료 순서는 고정되어 있지 않다.
세requestOrder()호출이 서로를 기다리지 않고 실행되기 때문에, 랜덤 대기 시간이 짧은 주문부터 완료된다.
이 예제의 최종 결과물은 세 주문이async/await로 처리되고, 완료된 주문부터 성공 처리와finally처리가 실행되는Console화면이다.
Promise 체이닝 방식과 async await 방식 비교하기
앞의 예제와 이번 예제는 같은
orderCoffee()함수를 사용한다.
즉, 커피 주문 자체는 여전히Promise로 처리된다.
차이는Promise결과를 받는 방식이다.
앞의 예제는then(),catch(),finally()를 직접 이어 붙였다.
이번 예제는requestOrder()함수 안에서await,try,catch,finally를 사용했다.
두 방식은 결과적으로 비슷한 일을 한다.
성공하면 성공 메시지를 출력하고, 실패하면 실패 메시지를 출력할 수 있으며, 마지막에는 마무리 메시지를 출력한다.
차이를 정리하면 아래처럼 볼 수 있다.
Promise체이닝 방식은then(),catch(),finally()를 이어 붙인다.async/await방식은try,catch,finally블록 안에서 흐름을 나눈다.await는Promise결과를 기다렸다가 변수에 담을 수 있게 해 준다.async/await방식은 주문 처리 흐름을 위에서 아래로 읽기 쉽게 만든다.
async/await방식은Promise체이닝을 더 읽기 쉬운 함수 흐름으로 바꿔 주는 방식이다.
핵심 정리
이 예제의 핵심은 커피 주문
Promise를async/await로 처리하는 흐름이다.
orderCoffee()는 여전히Promise를 반환하고,requestOrder()는 그Promise결과를await로 기다린다.
이 흐름은 아래처럼 정리할 수 있다.
random.js는 주문마다 다른 대기 시간을 만든다.coffee.js는orderCoffee()함수를 정의한다.orderCoffee()는 커피 주문Promise를 반환한다.exam27_1.html은requestOrder()라는async함수를 만든다.requestOrder()안에서는await orderCoffee(name)으로 주문 결과를 기다린다.- 성공하면
try안에서[성공]메시지를 출력한다.- 실패하면
catch에서[실패]메시지를 출력할 수 있다.- 현재 코드에서는
isAvailable이true라서 성공 흐름만 실행된다.finally는 성공과 실패에 관계없이 마지막에 실행된다.requestOrder()세 번은 서로를 기다리지 않고 연속으로 호출된다.- 그래서 전체 완료 순서는
random()결과에 따라 달라질 수 있다.따라서 이 예제가 알려 주는 핵심은 이것이다.
async/await를 사용하면Promise기반 비동기 작업의 성공, 실패, 마무리 흐름을 더 읽기 쉽게 작성할 수 있지만, 여러async함수 호출을 순서대로 기다리려면 호출하는 쪽에서도await가 필요하다.
다음 예제에서는fetch()를 사용해서JSON응답을 받아 처리하는 흐름을 확인한다.
이 예제는
fetch()를 사용해서 서버에 있는JSON파일을 요청하고, 응답으로 받은 데이터를 화면에 출력하는 흐름을 보여 준다.
JSON은 데이터를 이름과 값의 형태로 표현하는 데이터 형식이다.
JavaScript에서 객체처럼 다루기 좋기 때문에 서버와 데이터를 주고받을 때 자주 사용한다.
이번 예제에서는content/sample.json파일을 요청한다.
요청이 성공하면 응답 데이터를response.json()으로 변환한다.
그다음 변환된 객체를jsonToStr()함수에 전달해서 화면에 출력한다.
같은 기능을 두 가지 방식으로 확인한다.
첫 번째는then()을 이어 붙이는 방식이다.
두 번째는async/await를 사용하는 방식이다.
두 방식 모두 결과는 같지만, 코드 흐름을 읽는 방식이 다르다.
이 예제의 핵심은fetch()로 받은 응답을 바로 사용할 수 있는 것이 아니라,response.json()으로 변환한 뒤JavaScript객체처럼 다룰 수 있다는 점이다.
예제 전체 코드
// exam10.html <!DOCTYPE html> <html> <head> <meta charset='UTF-8'> <title>Ajax테스트</title> </head> <body> <h2>fetch() 함수 사용 - JSON응답</h2> <hr> <script> function jsonToStr(obj) { console.log(obj) for(var i in obj) document.body.innerHTML += `<h3>${obj[i]}</h3>`; } fetch('content/sample.json') .then(response => { if(!response.ok) { throw new Error(`HTTP Error : ${response.status}`); } return response.json(); }) .then(obj => jsonToStr(obj)) .catch(err => console.error("잡았다!! : "+err)); </script> </body> </html>// exam10_1.html <!DOCTYPE html> <html> <head> <meta charset='UTF-8'> <title>Ajax테스트</title> </head> <body> <h2>fetch()- async/await 함수 사용 - JSON응답</h2> <hr> <script> // 1. 객체를 HTML에 출력하는 함수 function jsonToStr(obj) { console.log(obj); for ( let i in obj) { document.body.innerHTML += `<h3>${obj[i]}</h3>`; } } // 2. 데이터를 가져오는 비동기 함수 정의 async function fetchContent() { try { const response = await fetch('content/sample.json'); // 응답 상태 확인 if (!response.ok) { throw new Error(`HTTP Error : ${response.status}`); } // JSON 파싱 대기 const obj = await response.json(); // 데이터 처리 함수 호출 jsonToStr(obj); } catch (err) { // 에러 핸들링 console.error("잡았다!! : " + err); } } // 3. 함수 실행 fetchContent(); </script> </body> </html>이 코드는 크게 두 방식으로 나누어 볼 수 있다.
첫 번째exam10.html은fetch()뒤에then()을 연결해서 응답을 처리한다.
두 번째exam10_1.html은async함수 안에서await를 사용해 응답을 기다린다.
두 파일 모두 요청하는 대상은 같다.
요청 주소는content/sample.json이다.
이 파일에서JSON데이터를 받아온 뒤,jsonToStr()함수로 화면에 출력한다.
두 예제는 코드 모양은 다르지만,JSON파일 요청 → 응답 확인 →JSON변환 → 화면 출력이라는 흐름은 같다.
이 예제에서 확인할 핵심
fetch()는 서버나 파일에 요청을 보내는 함수이다.fetch('content/sample.json')은content폴더 안의sample.json파일을 요청한다.response.ok는 응답이 성공 범위인지 확인할 때 사용한다.response.json()은 응답 본문을JSON데이터로 변환한다.jsonToStr()은 변환된 객체를 화면에 출력하는 함수이다.for...in은 객체 안의 값을 하나씩 꺼낼 때 사용할 수 있다.then()방식은Promise뒤에 처리할 코드를 계속 연결한다.async/await방식은 비동기 작업을 위에서 아래로 읽기 쉽게 작성한다.- 요청 실패나 응답 오류는
catch()또는try/catch에서 처리할 수 있다.이 예제는
fetch()로JSON응답을 받아오고, 그 데이터를 화면에 출력하는 기본 흐름을 확인하는 예제이다.
fetch는 요청을 보내는 함수이다
fetch()는 서버나 파일에 요청을 보내는 함수이다.
이 예제에서는 외부 서버가 아니라 현재 프로젝트 안에 있는content/sample.json파일을 요청한다.
코드는 아래처럼 작성되어 있다.// exam10.html fetch('content/sample.json')이 코드는
content폴더 안의sample.json파일을 가져오겠다는 뜻이다.
fetch()는 요청 결과를 바로 데이터로 주지 않는다.
대신Promise를 반환한다.
그래서fetch()뒤에then()을 붙이거나,async함수 안에서await로 기다려야 한다.
fetch()는 요청 결과를 바로 반환하는 함수가 아니라, 나중에 응답을 받을 수 있는Promise를 반환한다.
then 방식으로 JSON 응답 처리하기
// exam10.html fetch('content/sample.json') .then(response => { if(!response.ok) { throw new Error(`HTTP Error : ${response.status}`); } return response.json(); }) .then(obj => jsonToStr(obj)) .catch(err => console.error("잡았다!! : "+err));
exam10.html은then()방식으로fetch()응답을 처리한다.
fetch()가 성공적으로 응답 객체를 받으면 첫 번째then()이 실행된다.
첫 번째then()의response는 서버나 파일 요청에 대한 응답 정보를 담은 객체이다.
여기에는 응답 상태 코드, 응답 성공 여부, 응답 본문을 읽는 메서드 등이 들어 있다.
먼저response.ok를 확인한다.
response.ok가false라면 정상 응답이 아니라고 보고throw new Error(...)로 오류를 발생시킨다.
throw는 오류를 직접 발생시키는 코드이다.
응답이 정상이면return response.json()을 실행한다.
response.json()은 응답 본문을JSON데이터로 변환한다.
이 변환도 비동기 작업이기 때문에 다시Promise를 반환한다.
그다음 두 번째then()에서 변환된 객체를 받는다.
받은 객체는obj라는 이름으로 들어오고,jsonToStr(obj)를 호출해서 화면에 출력한다.
마지막catch()는 중간에 오류가 발생했을 때 실행된다.
예를 들어 파일 경로가 틀렸거나, 응답 상태가 정상이 아니면catch()에서 오류를 확인할 수 있다.
then()방식은fetch()요청, 응답 확인,JSON변환, 화면 출력을 단계별로 이어 붙여 처리한다.
response.ok는 응답 성공 여부를 확인한다
// exam10.html if(!response.ok) { throw new Error(`HTTP Error : ${response.status}`); }
response.ok는 응답이 성공 범위인지 확인할 때 사용한다.
성공 범위의 응답이면true가 된다.
정상적으로 데이터를 받아오지 못한 응답이면false가 될 수 있다.
여기서!response.ok는 “응답이 정상적이지 않다면”이라는 뜻이다.
!는 반대 의미를 만드는 연산자이다.
즉,response.ok가false일 때 조건문 안으로 들어간다.
조건문 안에서는throw new Error(...)를 실행한다.
이렇게 오류를 발생시키면 뒤의 정상 처리 흐름으로 가지 않고,catch()쪽에서 오류를 처리할 수 있다.
response.ok를 확인하면 응답이 정상인지 먼저 검사한 뒤 데이터를 처리할 수 있다.
response.json은 응답 본문을 JSON 데이터로 바꾼다
// exam10.html return response.json();
fetch()로 받은response는 응답 정보 전체를 담고 있는 객체이다.
이 상태에서는 응답 본문을 바로 객체처럼 사용할 수 없다.
response.json()을 호출해야 응답 본문이JSON데이터로 변환된다.
그리고 변환된 결과를 다음then()에서 사용할 수 있다.
예를 들어sample.json에 여러 값이 들어 있다면,response.json()이후에는 그 값을JavaScript객체처럼 다룰 수 있다.
그래서 다음 단계에서jsonToStr(obj)로 전달할 수 있다.
fetch()의 응답 본문은response.json()으로 변환해야JavaScript에서 다룰 수 있는 데이터가 된다.
jsonToStr은 객체 값을 화면에 출력한다
// exam10.html function jsonToStr(obj) { console.log(obj) for(var i in obj) document.body.innerHTML += `<h3>${obj[i]}</h3>`; }
jsonToStr()은JSON에서 변환된 객체를 받아 화면에 출력하는 함수이다.
함수 이름은JSON데이터를 문자열처럼 화면에 보여 주기 위한 함수라는 의미로 이해할 수 있다.
먼저console.log(obj)로 객체 전체를 개발자 도구Console에 출력한다.
이렇게 하면 실제로 어떤 데이터가 들어왔는지 확인할 수 있다.
그다음for...in을 사용한다.
for...in은 객체의 속성 이름을 하나씩 꺼내는 반복문이다.
여기서는 꺼낸 속성 이름이i에 들어간다.
obj[i]는 객체에서 해당 속성의 값을 꺼내는 코드이다.
그리고document.body.innerHTML += ...를 사용해서body영역에<h3>태그를 추가한다.
즉, 객체 안의 값이 하나씩<h3>형태로 화면에 출력된다.
이 예제의 결과에서는자바스크립트,29,웹앱개발 전용 OOP 언어가 화면에 출력된다.
jsonToStr()은Console에는 객체 전체를 출력하고, 브라우저 화면에는 객체의 값을 하나씩 출력하는 함수이다.
async await 방식으로 JSON 응답 처리하기
// exam10_1.html async function fetchContent() { try { const response = await fetch('content/sample.json'); if (!response.ok) { throw new Error(`HTTP Error : ${response.status}`); } const obj = await response.json(); jsonToStr(obj); } catch (err) { console.error("잡았다!! : " + err); } } fetchContent();
exam10_1.html은async/await방식으로 같은 작업을 처리한다.
기능은exam10.html과 같다.
다만 코드를 읽는 방식이 더 위에서 아래로 보인다.
fetchContent()앞에는async가 붙어 있다.
async함수 안에서는await를 사용할 수 있다.
const response = await fetch('content/sample.json')는fetch()요청이 끝날 때까지 기다린 뒤 응답 객체를response에 저장한다.
그다음response.ok로 응답 상태를 확인한다.
응답이 정상이라면const obj = await response.json()을 실행한다.
이 코드는 응답 본문을JSON데이터로 변환할 때까지 기다린 뒤, 변환된 객체를obj에 저장한다.
마지막으로jsonToStr(obj)를 호출해서 화면에 출력한다.
중간에 오류가 발생하면catch구간에서"잡았다!!"와 함께 오류가 출력된다.
async/await방식은fetch()요청과JSON변환을 기다리는 흐름을 위에서 아래로 읽을 수 있게 해 준다.
then 방식과 async await 방식 비교하기
exam10.html과exam10_1.html은 같은 결과를 만든다.
둘 다content/sample.json을 요청하고, 응답을JSON으로 변환한 뒤 화면에 출력한다.
차이는Promise를 처리하는 방식이다.
then()방식은 요청 뒤에 처리할 코드를 계속 이어 붙인다.
async/await방식은 기다릴 작업 앞에await를 붙여서 순서대로 작성한다.
정리하면 아래처럼 볼 수 있다.
then()방식은fetch()뒤에.then()을 이어 붙여 처리한다.async/await방식은await fetch()로 응답을 기다린다.- 두 방식 모두
response.ok로 응답 상태를 확인한다.- 두 방식 모두
response.json()으로 응답 본문을 변환한다.- 두 방식 모두
jsonToStr()로 화면에 데이터를 출력한다.- 두 방식 모두 오류 처리가 필요하다.
then()과async/await는 결과가 다른 문법이 아니라, 같은 비동기 흐름을 다르게 작성하는 방식이다.
실제 출력 결과 확인하기
이 예제를 실행하면 브라우저 화면에는
sample.json에서 가져온 값들이<h3>태그로 출력된다.
또 개발자 도구Console에는JSON에서 변환된 객체가 출력된다.
실제 화면에는 아래 값들이 출력된다.// 화면 출력 결과 // 자바스크립트 // 29 // 웹앱개발 전용 OOP 언어개발자 도구
Console에는 아래처럼 객체가 출력된다.// Console 출력 결과 // {name: '자바스크립트', age: 29, kind: '웹앱개발 전용 OOP 언어'}흐름을 정리하면 아래와 같다.
// 실행 흐름 // fetch()로 sample.json 요청 // response.ok로 응답 상태 확인 // response.json()으로 JSON 변환 // console.log(obj)로 객체 확인 // document.body.innerHTML로 화면 출력이 흐름이 확인되면
fetch()로JSON응답을 받아 처리하는 기본 구조가 정상적으로 동작한 것이다.
결과물 뽑기
이 예제의 결과물은 브라우저 화면과 개발자 도구
Console을 함께 확인해서 뽑는다.
브라우저 화면에서는sample.json에서 가져온 값들이<h3>형태로 출력되는지 확인한다.
개발자 도구Console에서는 변환된 객체가 출력되는지 확인한다.
먼저exam10.html을 브라우저에서 실행한다.
화면에는자바스크립트,29,웹앱개발 전용 OOP 언어가 출력된다.
오른쪽 개발자 도구Console에는{name: '자바스크립트', age: 29, kind: '웹앱개발 전용 OOP 언어'}객체가 출력된다.// exam10.html 출력결과 // 화면 출력: 자바스크립트, 29, 웹앱개발 전용 OOP 언어 // Console 출력: {name: '자바스크립트', age: 29, kind: '웹앱개발 전용 OOP 언어'}
then()방식에서도fetch()로 받은 응답을response.json()으로 변환하면,JSON데이터를 객체처럼 사용할 수 있고 그 값을 화면에 출력할 수 있다.
다음으로exam10_1.html을 브라우저에서 실행한다.
화면과Console에는exam10.html과 같은 데이터가 출력된다.
차이는 코드 작성 방식이다.
exam10_1.html은async/await로fetch()응답과JSON변환을 기다린다.// exam10_1.html 출력결과 // 화면 출력: 자바스크립트, 29, 웹앱개발 전용 OOP 언어 // Console 출력: {name: '자바스크립트', age: 29, kind: '웹앱개발 전용 OOP 언어'}
async/await방식도 결과는 같다.
다만await fetch()와await response.json()을 사용해서 요청과 변환 과정을 위에서 아래로 읽기 쉽게 작성한다.
이 결과에서 중요한 부분은 두 가지이다.
첫 번째는 브라우저 화면에JSON값들이 출력되는지이다.
두 번째는Console에 객체 형태의 데이터가 출력되는지이다.
화면에 값이 출력되었지만Console에 오류가 있다면 요청이나 변환 과정에서 문제가 있었을 수 있다.
반대로Console에는 객체가 출력되었는데 화면에 값이 없다면jsonToStr()의 화면 출력 코드가 제대로 실행되었는지 확인해야 한다.
이 예제의 최종 결과물은 브라우저 화면에 출력된JSON값과, 개발자 도구Console에 출력된 객체를 함께 확인하는 화면이다.
핵심 정리
이 예제의 핵심은
fetch()로JSON응답을 받아 처리하는 흐름이다.
fetch()는 요청을 보내고, 응답이 오면 그 응답을JSON데이터로 변환해서 사용할 수 있다.
이 흐름은 아래처럼 정리할 수 있다.
fetch()는 요청을 보내고Promise를 반환한다.content/sample.json은 요청할JSON파일 경로이다.response.ok로 응답이 정상인지 확인한다.response.json()으로 응답 본문을JSON데이터로 변환한다.- 변환된 객체는
obj로 전달된다.console.log(obj)로 객체 전체를 확인한다.for...in으로 객체의 값을 하나씩 꺼낸다.document.body.innerHTML에<h3>태그를 추가해 화면에 출력한다.then()방식과async/await방식은 같은 결과를 만드는 다른 작성 방식이다.따라서 이 예제가 알려 주는 핵심은 이것이다.
fetch()로 받은 응답은 바로 객체처럼 사용할 수 없고,response.json()으로 변환한 뒤JavaScript데이터처럼 다루어야 한다.
다음 예제에서는 다른 출처로 요청을 보낼 때 발생할 수 있는CORS흐름을 확인한다.
이 예제는 브라우저에서 서버로 비동기 요청을 보낼 때
XMLHttpRequest방식과fetch()방식이 어떻게 다른지 비교하는 예제이다.
두 예제는 모두http://localhost:9000/home으로 요청을 보내고, 서버가 응답한 문자열을 브라우저 화면에 출력한다.
exam11.html은XMLHttpRequest객체를 직접 만들어 요청을 보낸다.
exam11_1.html은fetch()함수를 사용해 같은 주소로 요청을 보낸다.
요청 문법은 다르지만, 최종 목적은 같다.
두 방식 모두/home에서 받은 응답 문자열을 화면에 추가한다.
이 예제의 핵심은XMLHttpRequest와fetch()모두 서버에 비동기 요청을 보낼 수 있으며, 다른 출처로 요청할 때는CORS정책의 영향을 받을 수 있다는 점이다.
예제 전체 코드
// exam11.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> </head> <body> <h1>CORS 스펙을 만족하지 않는 서비스 요청</h1> <hr> <script> window.onload = function (e) { // XMLHttpRequest 객체를 생성한다. let xhr = new XMLHttpRequest(); // 응답을 정상적으로 받았을 때 실행된다. xhr.onload = function() { document.body.innerHTML += `<h3>${xhr.responseText}</h3>`; }; // GET 방식으로 /home 주소에 요청을 보낸다. xhr.open("GET", "http://localhost:9000/home", true); // 실제 요청을 전송한다. xhr.send(); }; </script> </body> </html>// exam11_1.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> </head> <body> <h1>CORS 스펙을 만족하지 않는 서비스 요청</h1> <hr> <script> window.onload = function () { // fetch()로 /home 주소에 요청을 보낸다. fetch("http://localhost:9000/home") .then(response => { // 네트워크 응답이 성공 범위인지 확인한다. if (!response.ok) { throw new Error(`HTTP 에러 발생! 상태코드: ${response.status}`); } // 응답 본문을 문자열로 받는다. return response.text(); }) .then(data => { // 받아온 문자열을 화면에 추가한다. document.body.innerHTML += `<h3>${data}</h3>`; }) .catch(error => { // 요청이나 응답 처리 중 오류가 발생하면 Console에 출력한다. console.error("데이터를 가져오는 중 오류가 발생했습니다:", error); }); }; </script> </body> </html>이 코드는 크게 두 파일로 나누어 볼 수 있다.
첫 번째exam11.html은XMLHttpRequest객체를 직접 만들어 요청을 보내는 방식이다.
두 번째exam11_1.html은fetch()함수를 사용해서 같은 요청을 보내는 방식이다.
두 파일 모두 요청 주소는 같다.
요청 대상은http://localhost:9000/home이다.
이 주소는 서버의/home요청을 의미한다.
현재 결과물에서는HTML파일도localhost:9000에서 실행되고, 요청 대상도localhost:9000/home이다.
그래서 브라우저 화면에/home응답 문자열인CORS 설정을 하지 않았어요ㅠㅠ가 출력된다.
현재 실행 결과는CORS오류가 발생한 화면이 아니라, 서버가 반환한 문자열이 정상적으로 화면에 출력된 화면이다.
이 예제에서 확인할 핵심
XMLHttpRequest는 예전부터 사용된AJAX요청 객체이다.fetch()는Promise기반으로 요청을 보내는 비교적 새로운 방식이다.- 두 방식 모두 서버로 비동기 요청을 보낼 수 있다.
exam11.html은XMLHttpRequest로/home요청을 보낸다.exam11_1.html은fetch()로/home요청을 보낸다./home응답은 문자열이다.XMLHttpRequest방식에서는xhr.responseText로 응답 문자열을 읽는다.fetch()방식에서는response.text()로 응답 문자열을 읽는다.- 다른 출처로 요청하면 서버의
CORS허용 설정이 필요할 수 있다.이 예제는 같은 서버 요청을
XMLHttpRequest와fetch()두 방식으로 보내고, 응답 문자열을 화면에 출력하는 흐름을 비교하는 예제이다.
CORS란 무엇인가
CORS는Cross-Origin Resource Sharing의 줄임말이다.
쉽게 말하면 서로 다른 출처끼리 자원을 주고받을 수 있게 허용하는 규칙이다.
여기서 자원은 서버가 응답하는 문자열,JSON, 이미지, 파일 같은 데이터를 의미한다.
브라우저는 보안을 위해 아무 사이트에서나 다른 서버의 응답을 마음대로 읽지 못하게 한다.
예를 들어 어떤 페이지가 사용자의 허락 없이 다른 서버에 요청을 보내고 응답을 마음대로 읽으면 보안 문제가 생길 수 있다.
그래서 브라우저는 요청을 보낸 페이지의 출처와 요청 대상 서버의 출처를 비교한다.
출처가 다르면 서버가 “이 출처의 요청을 허용한다”는 응답 헤더를 보내야 한다.
그 설정이 없으면 브라우저는 응답을JavaScript코드에서 사용할 수 없게 막을 수 있다.
CORS는 서버 요청 자체를 무조건 막는 규칙이 아니라, 다른 출처의 응답을 브라우저가JavaScript에서 사용해도 되는지 판단하는 규칙이다.
출처는 protocol, host, port로 판단한다
출처는 단순히 같은 컴퓨터인지 아닌지로 판단하지 않는다.
브라우저는protocol,host,port를 함께 보고 출처를 구분한다.
예를 들어 현재 결과물의 실행 주소는 아래와 같다.
http://localhost:9000/exam11.html
이 주소의 출처는 아래처럼 볼 수 있다.
protocol:httphost:localhostport:9000요청 대상 주소는 아래와 같다.
http://localhost:9000/home
이 주소의 출처는 아래처럼 볼 수 있다.
protocol:httphost:localhostport:9000두 주소는
protocol,host,port가 같다.
그래서 현재 실행 결과에서는 같은 출처 요청으로 볼 수 있고, 응답 문자열이 화면에 출력된다.
반대로HTML파일을http://127.0.0.1:5500에서 실행하고, 요청은http://localhost:9000/home으로 보내면 출처가 달라질 수 있다.
이 경우 서버가CORS를 허용하지 않으면 브라우저가 응답 사용을 막을 수 있다.
같은 내 컴퓨터에서 실행하더라도protocol,host,port중 하나라도 다르면 브라우저는 다른 출처로 판단할 수 있다.
XMLHttpRequest 방식으로 요청하기
// exam11.html window.onload = function (e) { let xhr = new XMLHttpRequest(); // 요청 객체를 생성한다. xhr.onload = function() { document.body.innerHTML += `<h3>${xhr.responseText}</h3>`; // 응답 문자열을 화면에 추가한다. }; xhr.open("GET", "http://localhost:9000/home", true); // GET 비동기 요청을 준비한다. xhr.send(); // 요청을 전송한다. };
XMLHttpRequest는 브라우저에서 서버에 비동기 요청을 보내기 위해 사용하던 객체이다.
AJAX요청의 오래된 방식이라고 이해하면 된다.
먼저new XMLHttpRequest()로 요청 객체를 만든다.
이 객체를 통해 요청을 준비하고 전송할 수 있다.
xhr.onload는 응답을 정상적으로 받았을 때 실행할 함수를 지정한다.
응답이 정상적으로 도착하면xhr.responseText에 응답 문자열이 들어간다.
이 예제에서는 그 문자열을<h3>태그로body에 추가한다.
xhr.open("GET", "http://localhost:9000/home", true)는 요청을 준비하는 코드이다.
첫 번째 값인"GET"은 요청 방식이다.
두 번째 값은 요청 주소이다.
세 번째 값인true는 비동기 방식으로 요청하겠다는 의미이다.
마지막으로xhr.send()가 실제 요청을 보낸다.
XMLHttpRequest방식은 요청 객체를 만들고, 요청을 열고, 전송하는 단계를 직접 작성한다.
fetch 방식으로 같은 요청 보내기
// exam11_1.html window.onload = function () { fetch("http://localhost:9000/home") .then(response => { if (!response.ok) { throw new Error(`HTTP 에러 발생! 상태코드: ${response.status}`); } return response.text(); }) .then(data => { document.body.innerHTML += `<h3>${data}</h3>`; }) .catch(error => { console.error("데이터를 가져오는 중 오류가 발생했습니다:", error); }); };
fetch()는 서버에 요청을 보내는 함수이다.
XMLHttpRequest보다 코드가 간결하고,Promise기반으로 동작한다.
fetch("http://localhost:9000/home")은/home주소로 요청을 보낸다.
fetch()는 응답 결과를 바로 문자열로 주지 않고, 응답 객체를 담은Promise를 반환한다.
그래서 뒤에then()을 붙여 응답을 처리한다.
첫 번째then()에서는response.ok를 확인한다.
응답이 정상 범위가 아니면throw new Error(...)로 오류를 발생시킨다.
응답이 정상이면response.text()를 호출한다.
response.text()는 응답 본문을 문자열로 읽는 메서드이다.
이 예제의/home응답은 문자열이므로response.json()이 아니라response.text()를 사용한다.
두 번째then()에서는 문자열로 변환된 응답을data로 받는다.
그리고document.body.innerHTML += ...를 사용해 화면에 추가한다.
마지막catch()는 요청이나 응답 처리 과정에서 발생한 오류를Console에 출력한다.
fetch()방식은 요청 결과를Promise로 받고,then()과catch()로 성공과 실패 흐름을 이어서 처리한다.
XMLHttpRequest와 fetch의 차이
XMLHttpRequest와fetch()는 둘 다 서버로 비동기 요청을 보낼 수 있다.
즉, 둘 다AJAX요청을 만들 수 있다.
하지만 코드 작성 방식은 다르다.
XMLHttpRequest는 요청 객체를 직접 만들고,open(),send(),onload같은 속성과 메서드를 사용한다.
반면fetch()는 함수 호출 하나로 요청을 시작하고,Promise체이닝으로 응답을 처리한다.
정리하면 아래처럼 볼 수 있다.
XMLHttpRequest는 요청 객체를 직접 생성한다.XMLHttpRequest는open()으로 요청을 준비하고send()로 전송한다.XMLHttpRequest는onload에 응답 처리 함수를 등록한다.XMLHttpRequest는 응답 문자열을xhr.responseText로 읽는다.fetch()는 함수 호출로 요청을 시작한다.fetch()는Promise를 반환한다.fetch()는then()과catch()로 응답과 오류를 처리한다.fetch()는 응답 문자열을response.text()로 읽는다.두 방식은 문법은 다르지만, 다른 출처로 요청할 때는 둘 다
CORS정책의 영향을 받을 수 있다.
XMLHttpRequest와fetch()의 차이는 요청 문법의 차이이고, 서버로 비동기 요청을 보낸다는 목적은 같다.
CORS 오류가 발생하는 경우
이 예제의 제목은
CORS스펙을 만족하지 않는 서비스 요청이다.
즉, 다른 출처로 요청을 보내면 문제가 생길 수 있다는 상황을 보여 주기 위한 예제이다.
현재 결과물에서는 페이지 주소가localhost:9000이고, 요청 대상도localhost:9000/home이다.
그래서 응답 문자열이 화면에 출력된다.
하지만HTML파일을127.0.0.1:5500에서 실행하고, 요청을localhost:9000/home으로 보내면 브라우저는 다른 출처 요청으로 볼 수 있다.
이때 서버가CORS허용 헤더를 보내지 않으면 브라우저가 응답 사용을 막을 수 있다.
이 상황에서는 서버가 실제로 응답을 했더라도 화면에 응답 문자열이 출력되지 않을 수 있다.
대신 개발자 도구Console에CORS관련 오류가 나타날 수 있다.
CORS오류는 요청 코드가 무조건 틀렸다는 뜻이 아니라, 브라우저가 다른 출처의 응답 사용을 보안 정책상 막았다는 뜻이다.
실제 실행 흐름 확인하기
이 예제를 실행하면 브라우저는
HTML파일을 먼저 로드한다.
그다음window.onload가 실행되면서/home요청을 보낸다.
exam11.html의 실행 흐름은 아래와 같다.// exam11.html 실행 흐름 // HTML 문서 로드 완료 // window.onload 실행 // XMLHttpRequest 객체 생성 // GET http://localhost:9000/home 요청 준비 // xhr.send()로 요청 전송 // 응답을 받으면 xhr.onload 실행 // xhr.responseText를 화면에 출력
exam11_1.html의 실행 흐름은 아래와 같다.// exam11_1.html 실행 흐름 // HTML 문서 로드 완료 // window.onload 실행 // fetch("http://localhost:9000/home") 요청 // response.ok 확인 // response.text()로 문자열 응답 변환 // 변환된 문자열을 화면에 출력 // 오류가 발생하면 catch()에서 Console 출력두 흐름 모두 최종 목적은 같다.
http://localhost:9000/home에서 받은 응답 문자열을 화면에 추가하는 것이다.
다만 요청 방식과 응답 처리 문법이 다르다.
또 실행 주소와 요청 주소의 출처가 달라지면CORS정책의 영향을 받을 수 있다.
실행 흐름의 핵심은HTML로드 후/home으로 요청을 보내고, 받은 문자열 응답을 화면에 출력하는 것이다.
결과물 뽑기
이 예제의 결과물은 브라우저 화면에서
/home응답 문자열이 출력되는지 확인하면 된다.
현재 실행 결과에서는 두 파일 모두localhost:9000에서 실행되었고,/home응답 문자열이 화면에 출력되었다.
먼저exam11.html을 브라우저에서 실행한다.
XMLHttpRequest방식으로http://localhost:9000/home에 요청을 보내고, 응답 문자열을xhr.responseText로 받아 화면에 출력한다.// exam11.html 출력결과 // CORS 설정을 하지 않았어요ㅠㅠ
XMLHttpRequest로/home에 요청을 보내면, 응답 문자열이xhr.responseText에 들어가고 화면에 출력된다.
다음으로exam11_1.html을 브라우저에서 실행한다.
이번에는fetch()로 같은/home요청을 보낸다.
응답 문자열은response.text()를 통해 읽고, 화면에 출력한다.// exam11_1.html 출력결과 // CORS 설정을 하지 않았어요ㅠㅠ
fetch()방식도/home에서 받은 문자열 응답을response.text()로 읽은 뒤 화면에 출력한다.
이 결과에서 중요한 부분은 두 방식 모두 같은 응답 문자열을 출력한다는 점이다.
XMLHttpRequest는xhr.responseText로 문자열을 읽고,fetch()는response.text()로 문자열을 읽는다.
이 예제의 최종 결과물은XMLHttpRequest방식과fetch()방식 모두/home응답 문자열을 화면에 출력한 결과 화면이다.
핵심 정리
이 예제의 핵심은
CORS상황을 염두에 두고, 두 가지AJAX요청 방식을 비교하는 것이다.
XMLHttpRequest와fetch()는 모두 비동기 요청을 보낼 수 있지만, 작성 방식이 다르다.
이 흐름은 아래처럼 정리할 수 있다.
CORS는 다른 출처의 응답을 브라우저가 사용할 수 있는지 판단하는 보안 정책이다.- 출처는
protocol,host,port를 기준으로 판단한다.- 현재 결과물에서는 페이지와 요청 대상이 모두
localhost:9000이므로 응답 문자열이 화면에 출력된다.exam11.html은XMLHttpRequest로/home요청을 보낸다.exam11_1.html은fetch()로/home요청을 보낸다.XMLHttpRequest는open(),send(),onload방식으로 요청을 처리한다.XMLHttpRequest는 문자열 응답을xhr.responseText로 읽는다.fetch()는Promise기반으로 요청을 보내고then(),catch()로 처리한다.fetch()는 문자열 응답을response.text()로 읽는다.- 실행 주소와 요청 주소의 출처가 다르면
CORS정책의 영향을 받을 수 있다.따라서 이 예제가 알려 주는 핵심은 이것이다.
XMLHttpRequest와fetch()는 요청 문법은 다르지만, 서버로 비동기 요청을 보내는 목적은 같고, 다른 출처로 요청할 때는 둘 다 브라우저의CORS정책을 고려해야 한다.
다음 예제에서는AJAX로JSON데이터를POST요청 본문에 담아 서버로 전달하는 흐름을 확인한다.
이 예제는
AJAX로JSON데이터를 서버에POST요청으로 보내고, 서버 응답을 다시 화면에 출력하는 흐름을 보여 준다.
POST요청은 데이터를 서버로 보낼 때 자주 사용하는 요청 방식이다.
이번 예제에서는 같은 화면 안에 두 가지 전송 방식이 함께 들어 있다.
첫 번째는form태그를 통한 일반POST요청이다.
두 번째는 버튼을 클릭했을 때fetch()로JSON데이터를 보내는AJAX요청이다.
form전송은 브라우저가 화면 이동을 포함해서 요청을 처리하는 방식이다.
반면AJAX전송은 현재 화면을 유지한 상태에서JavaScript가 서버에 요청을 보내고, 응답을 받아 화면 일부만 바꾸는 방식이다.
이 예제의 핵심은 버튼의data-*속성에서 값을 꺼내 객체로 만들고, 그 객체를JSON.stringify()로JSON문자열로 바꾼 뒤POST요청 본문에 담아 보내는 흐름이다.
예제 전체 코드
// exam12.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>JSP test example</title> </head> <body> <h1>RequestBody Test!!!!</h1> <hr> <h2 id="msg_area"></h2> <hr> <h3><FORM> 태그를 통한 요청</h3> <form method="post" action="http://localhost:9000/rb1"> <input type="text" name="name" value="유니코"><br> <input type="number" name="age" value="20"><br> <input type="submit" value="전송"> </form> <hr> <h3>AJAX를 통한 요청</h3> <button id="rb2" data-pname="둘리" data-page="10">데이터 전송 1</button> <button id="rb3" data-pname="또치" data-page="11">데이터 전송 2</button> <button id="rb4" data-pname="도우너" data-page="12">데이터 전송 3</button> <script> const buttons = document.querySelectorAll("button"); const postData = event => { console.log("데이터 전송"); fetch("http://localhost:9000/"+event.target.id, { method: 'post', headers: {'content-type': 'application/json'}, body : JSON.stringify({ name : event.target.getAttribute("data-pname"), age : event.target.getAttribute("data-page") }) }) .then((response) => response.json()) .then((data) => { console.log('성공:', data); document.querySelector("#msg_area").textContent = `${data.name}:${data.age}`; }); }; buttons.forEach( elem => elem.addEventListener("click", postData)); </script> </body> </html>// exam12_1.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>JSP test example</title> </head> <body> <h1>RequestBody Test!!!!</h1> <hr> <h2 id="msg_area" ></h2> <hr> <h3><FORM> 태그를 통한 요청</h3> <form method="post" action="http://localhost:9000/rb1"> <input type="text" name="name" value="유니코"><br> <input type="number" name="age" value="20"><br> <input type="submit" value="전송"> </form> <hr> <h3>AJAX를 통한 요청</h3> <button id="rb2" data-pname="둘리" data-page="10">데이터 전송 1</button> <button id="rb3" data-pname="또치" data-page="11">데이터 전송 2</button> <button id="rb4" data-pname="도우너" data-page="12">데이터 전송 3</button> <script> const buttons = document.querySelectorAll("button"); // async 키워드를 붙여 비동기 함수로 정의한다. const postData = async (event) => { console.log("데이터 전송"); // 전송할 데이터를 미리 객체로 준비한다. const payload = { name: event.target.getAttribute("data-pname"), age: event.target.getAttribute("data-page") }; try { // fetch 요청을 보내고 응답을 기다린다. const response = await fetch("http://localhost:9000/" + event.target.id, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); // 응답 상태를 확인한다. if (!response.ok) { throw new Error(`서버 응답 오류: ${response.status}`); } // 응답 본문을 JSON으로 변환한다. const data = await response.json(); // 결과를 Console과 화면에 출력한다. console.log('성공:', data); document.querySelector("#msg_area").textContent = `${data.name}:${data.age}`; } catch (error) { // 에러 발생 시 Console에 출력한다. console.error('데이터 전송 실패:', error); } }; buttons.forEach(elem => elem.addEventListener("click", postData)); </script> </body> </html>이 코드는 크게 두 파일로 나누어 볼 수 있다.
첫 번째exam12.html은then()방식으로POST요청 결과를 처리한다.
두 번째exam12_1.html은async/await방식으로 같은 요청 흐름을 처리한다.
두 파일 모두 화면 구조는 같다.
위쪽에는form태그를 통한 요청이 있고, 아래쪽에는AJAX요청을 보내는 버튼 세 개가 있다.
AJAX요청 버튼은 각각rb2,rb3,rb4라는id를 가진다.
버튼을 클릭하면 이id값이 요청 주소 뒤에 붙는다.
예를 들어rb2버튼을 클릭하면http://localhost:9000/rb2로 요청을 보낸다.
두 예제는 코드 작성 방식은 다르지만, 버튼 클릭 → 값 꺼내기 →JSON변환 →POST요청 → 응답 출력 흐름은 같다.
이 예제에서 확인할 핵심
form태그는 일반POST요청을 보내지만,AJAX처럼JSON요청을 보내는 방식은 아니다.form태그 요청은 기본적으로application/x-www-form-urlencoded형식으로 입력값을 전송한다.AJAX요청은 현재 화면을 유지한 채 서버와 데이터를 주고받는다.button의data-pname,data-page속성에 전송할 값이 들어 있다.event.target은 실제 클릭된 버튼을 가리킨다.getAttribute()는 태그의 속성 값을 꺼낸다.JSON.stringify()는 객체를JSON문자열로 변환한다.AJAX요청에서Content-Type: application/json은 요청 본문이JSON형식이라는 뜻이다.fetch()는 서버에POST요청을 보내고Promise를 반환한다.- 서버 응답은
response.json()으로 변환한 뒤 사용할 수 있다.AJAX응답 결과는#msg_area영역에이름:나이형태로 출력된다.이 예제는
form태그를 통한 일반POST요청과, 버튼에 저장된 데이터를 꺼내JSON요청 본문으로 만들어AJAX로 전송하는 흐름을 비교하는 예제이다.
form 태그를 통한 요청과 AJAX 요청의 차이
// exam12.html <form method="post" action="http://localhost:9000/rb1"> <input type="text" name="name" value="유니코"><br> <input type="number" name="age" value="20"><br> <input type="submit" value="전송"> </form>
form태그는 브라우저가 기본으로 제공하는 전송 방식이다.
submit버튼을 누르면action에 적힌 주소로 요청을 보낸다.
여기서는http://localhost:9000/rb1로POST요청을 보낸다.
form안의input에는name속성이 있다.
서버는 이name속성 기준으로 값을 받을 수 있다.
예를 들어name="name"인 입력값은유니코이고,name="age"인 입력값은20이다.
이때form태그 요청은JSON데이터를 보내는 방식이 아니다.
일반적으로form태그는 입력값을name=유니코&age=20처럼form데이터 형식으로 묶어서 보낸다.
즉, 같은POST요청이라도AJAX에서 보내는JSON요청과 요청 본문 형식이 다르다.
반면 아래쪽의AJAX요청은form을 제출하지 않는다.
버튼 클릭 이벤트를 잡아서fetch()로 직접 요청을 보낸다.
그리고Content-Type을application/json으로 설정한 뒤,JSON.stringify()로 만든JSON문자열을 요청 본문에 담아 보낸다.
그래서 화면 이동 없이 응답 결과를 받아 현재 화면의#msg_area만 바꿀 수 있다.
form요청은 브라우저가 일반form데이터 전송을 처리하고,AJAX요청은JavaScript가JSON요청과 응답 처리를 직접 제어한다.
버튼의 data 속성에는 전송할 값이 들어 있다
// exam12.html <button id="rb2" data-pname="둘리" data-page="10">데이터 전송 1</button> <button id="rb3" data-pname="또치" data-page="11">데이터 전송 2</button> <button id="rb4" data-pname="도우너" data-page="12">데이터 전송 3</button>버튼에는
id,data-pname,data-page속성이 들어 있다.
id는 요청 주소를 만들 때 사용된다.
data-pname은 서버로 보낼 이름 값이다.
data-page는 서버로 보낼 나이 값이다.
data-*속성은HTML태그에 개발자가 직접 데이터를 저장할 때 사용하는 속성이다.
여기서data-pname과data-page는 버튼마다 다른 값을 담고 있다.
각 버튼의 값은 아래처럼 정리할 수 있다.
rb2버튼은둘리,10을 보낸다.rb3버튼은또치,11을 보낸다.rb4버튼은도우너,12를 보낸다.버튼을 클릭하면 클릭된 버튼의
data-*값이 요청 본문에 들어간다.
data-*속성은 화면 요소에 필요한 데이터를 숨겨 두었다가, 이벤트가 발생했을 때 꺼내 쓰기 위한 속성이다.
모든 버튼에 click 이벤트를 연결한다
// exam12.html const buttons = document.querySelectorAll("button"); buttons.forEach( elem => elem.addEventListener("click", postData));
document.querySelectorAll("button")은 문서 안의 모든button요소를 찾는다.
이 예제에서는데이터 전송 1,데이터 전송 2,데이터 전송 3버튼이 선택된다.
그다음forEach()로 버튼을 하나씩 꺼낸다.
각 버튼에는addEventListener("click", postData)가 연결된다.
addEventListener()는 특정 이벤트가 발생했을 때 실행할 함수를 등록한다.
여기서는 버튼을 클릭하면postData함수가 실행되도록 연결한다.
세 버튼은 같은postData함수를 공유하지만, 실제 전송되는 값은 클릭된 버튼의data-*속성에 따라 달라진다.
event.target은 클릭된 버튼을 가리킨다
// exam12.html const postData = event => { console.log("데이터 전송"); fetch("http://localhost:9000/"+event.target.id, { method<: 'post', headers: {'content-type': 'application/json'}, body : JSON.stringify({ name : event.target.getAttribute("data-pname"), age : event.target.getAttribute("data-page") }) }) }
postData함수는 버튼 클릭 이벤트가 발생했을 때 실행된다.
이때 함수의 매개변수event에는 클릭 이벤트 정보가 들어 있다.
event.target은 이벤트가 실제로 발생한 대상을 의미한다.
여기서는 사용자가 클릭한 버튼이event.target이 된다.
예를 들어데이터 전송 1버튼을 클릭하면event.target.id는rb2이다.
그래서 요청 주소는 아래처럼 만들어진다.// 요청 주소 예시 // http://localhost:9000/rb2또
event.target.getAttribute("data-pname")은 클릭된 버튼의data-pname값을 꺼낸다.
event.target.getAttribute("data-page")는 클릭된 버튼의data-page값을 꺼낸다.
event.target을 사용하면 여러 버튼이 같은 함수를 사용해도, 실제 클릭된 버튼의 값을 구분해서 처리할 수 있다.
JSON.stringify로 객체를 JSON 문자열로 바꾼다
// exam12.html body : JSON.stringify({ name : event.target.getAttribute("data-pname"), age : event.target.getAttribute("data-page") })
fetch()로JSON데이터를 보낼 때는 요청 본문에 객체를 그대로 넣지 않는다.
객체를JSON문자열로 바꿔서 보내야 한다.
이때 사용하는 함수가JSON.stringify()이다.
예를 들어데이터 전송 1버튼을 클릭하면 아래 객체가 만들어진다.// 전송 전 객체 // { // name: "둘리", // age: "10" // }이 객체는
JSON.stringify()를 거쳐 아래와 같은JSON문자열이 된다.// 전송되는 JSON 문자열 // {"name":"둘리","age":"10"}이 문자열이
fetch()요청의body에 들어간다.
서버는 요청 본문에 들어 있는 이JSON데이터를 읽어서 처리한다.
JSON.stringify()는JavaScript객체를 서버로 보낼 수 있는JSON문자열 형태로 바꾸는 함수이다.
Content-Type은 요청 본문 형식을 알려 준다
// exam12.html headers: {'content-type': 'application/json'}
headers는 요청에 대한 추가 정보를 담는 부분이다.
여기서는content-type을application/json으로 설정했다.
Content-Type: application/json은 요청 본문이JSON형식이라는 뜻이다.
이 설정은form태그 요청이 아니라,fetch()로 보내는AJAX요청에 적용된다.
서버는 이 정보를 보고 요청 본문을 어떤 방식으로 읽을지 판단할 수 있다.
만약JSON문자열을 보내면서Content-Type을 제대로 알려 주지 않으면, 서버가 요청 본문을 기대한 방식으로 읽지 못할 수 있다.
AJAX로JSON데이터를 요청 본문에 보낼 때는Content-Type을application/json으로 설정해야 한다.
then 방식으로 POST 응답 처리하기
// exam12.html fetch("http://localhost:9000/"+event.target.id, { method: 'post', headers: {'content-type': 'application/json'}, body : JSON.stringify({ name : event.target.getAttribute("data-pname"), age : event.target.getAttribute("data-page") }) }) .then((response) => response.json()) .then((data) => { console.log('성공:', data); document.querySelector("#msg_area").textContent = `${data.name}:${data.age}`; });
exam12.html은then()방식으로POST요청 응답을 처리한다.
fetch()는 요청을 보내고 응답을 담은Promise를 반환한다.
첫 번째then()에서는response.json()을 호출한다.
서버가JSON응답을 반환한다고 보고, 응답 본문을JavaScript객체로 변환하는 것이다.
두 번째then()에서는 변환된 데이터를data로 받는다.
console.log('성공:', data)로 응답 객체를Console에 출력한다.
그다음document.querySelector("#msg_area").textContent = ...로 화면의h2영역에 결과를 출력한다.
예를 들어 서버가{name: "둘리", age: 10}형태의 응답을 보내면 화면에는둘리:10이 출력된다.
then()방식은POST요청 →JSON응답 변환 → 화면 출력 흐름을 단계별로 이어 붙여 처리한다.
async await 방식으로 POST 응답 처리하기
// exam12_1.html const postData = async (event) => { console.log("데이터 전송"); const payload = { name: event.target.getAttribute("data-pname"), age: event.target.getAttribute("data-page") }; try { const response = await fetch("http://localhost:9000/" + event.target.id, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!response.ok) { throw new Error(`서버 응답 오류: ${response.status}`); } const data = await response.json(); console.log('성공:', data); document.querySelector("#msg_area").textContent = `${data.name}:${data.age}`; } catch (error) { console.error('데이터 전송 실패:', error); } };
exam12_1.html은async/await방식으로 같은 작업을 처리한다.
먼저payload객체를 따로 만든다.
payload에는 클릭된 버튼에서 꺼낸name,age값이 들어간다.
그다음await fetch(...)로POST요청이 끝날 때까지 기다린다.
응답이 돌아오면response.ok로 정상 응답인지 확인한다.
정상 응답이 아니라면throw new Error(...)로 오류를 발생시킨다.
응답이 정상이면await response.json()으로 응답 본문을JSON데이터로 변환한다.
변환된 결과는data에 들어간다.
마지막으로Console에 응답 객체를 출력하고,#msg_area에이름:나이형태로 화면 출력한다.
async/await방식은 요청 전송, 응답 확인,JSON변환, 화면 출력을 위에서 아래로 읽기 쉽게 작성한다.
then 방식과 async await 방식 비교하기
exam12.html과exam12_1.html은 같은 일을 한다.
둘 다 버튼을 클릭하면 버튼의data-*값을 꺼내JSON요청 본문으로 만들고, 서버 응답을 받아 화면에 출력한다.
차이는 응답을 처리하는 방식이다.
exam12.html은then()을 이어 붙여 처리한다.
exam12_1.html은async/await와try/catch를 사용해 처리한다.
정리하면 아래처럼 볼 수 있다.
then()방식은fetch()뒤에.then()을 이어 붙인다.async/await방식은await fetch()로 응답을 기다린다.then()방식은 요청 본문 객체를JSON.stringify({...})안에서 바로 만든다.async/await방식은payload객체를 먼저 만들고, 그 객체를JSON.stringify(payload)로 변환한다.- 두 방식 모두
AJAX요청에서Content-Type을application/json으로 설정한다.- 두 방식 모두 응답을
response.json()으로 변환한다.- 두 방식 모두
#msg_area에이름:나이형태로 결과를 출력한다.두 방식은 결과가 다른 것이 아니라, 같은
AJAXPOST요청 흐름을 서로 다른 문법으로 작성한 것이다.
실제 출력 결과 확인하기
이 예제의 출력 결과는
form태그 요청과AJAX요청을 나누어 확인하는 것이 좋다.
두 요청은 모두POST요청을 사용하지만, 브라우저에서 처리되는 방식과 요청 본문 형식이 다르기 때문이다.
먼저form태그 요청은submit버튼을 누르면action에 작성된http://localhost:9000/rb1로 요청을 보낸다.
이때input의name속성 기준으로 값이 서버에 전달된다.
따라서name값에는유니코,age값에는20이 전달된다.// form 태그 요청 확인 결과 // 요청 주소: http://localhost:9000/rb1 // 전송 값: name=유니코, age=20
form태그 요청은 브라우저가 직접 요청을 처리한다.
그래서AJAX처럼 현재 화면의 일부만 바꾸는 방식이 아니라,action주소로 요청을 보내고 서버 응답을 브라우저가 받는 흐름으로 동작한다.
다음으로AJAX요청은 버튼을 클릭하면 화면의#msg_area에 서버 응답 값이 출력된다.
또 개발자 도구Console에는"데이터 전송"로그와성공:응답 객체가 출력된다.
버튼별 출력 결과는 아래처럼 볼 수 있다.// AJAX 요청 출력결과 // 데이터 전송 1 클릭: 둘리:10 // 데이터 전송 2 클릭: 또치:11 // 데이터 전송 3 클릭: 도우너:12
Console에는 아래와 비슷한 형태로 응답 객체가 출력된다.// Console 출력 예시 // 데이터 전송 // 성공: {name: '둘리', age: 10}여기서 중요한 점은 화면에 출력되는 값이 버튼의
data-*속성과 연결된다는 것이다.
데이터 전송 1버튼은data-pname="둘리",data-page="10"을 가지고 있으므로 화면에둘리:10이 출력된다.
form태그 요청은 브라우저가 화면 이동을 포함해 처리하고,AJAX요청은 현재 화면을 유지한 채 응답 결과만 화면 일부에 반영한다.
결과물 뽑기
이 예제의 결과물은 두 가지로 나누어 뽑는다.
첫 번째는form태그를 통한 일반POST요청 결과이다.
두 번째는AJAX를 통한JSONPOST요청 결과이다.
같은 화면에서 테스트하지만, 두 요청은 결과를 확인하는 기준이 다르다.
form태그 요청은submit버튼을 눌렀을 때 브라우저가action주소로 요청을 보내는지 확인한다.
AJAX요청은 버튼 클릭 후 현재 화면의#msg_area와 개발자 도구Console을 함께 확인한다.
먼저form태그 요청 결과를 확인한다.
exam12.html을 브라우저에서 실행한 뒤,form영역의전송버튼을 클릭한다.
이때input에 들어 있는유니코,20값이http://localhost:9000/rb1로POST요청된다.// form 태그 요청 확인 결과 // 요청 주소: http://localhost:9000/rb1 // 전송 값: name=유니코, age=20
form태그 요청은submit을 누르면 브라우저가action주소로POST요청을 보낸다.
이 방식은fetch()를 사용하지 않기 때문에,AJAX처럼 현재 화면의 일부만 바꾸는 흐름이 아니다.
다음으로AJAX요청 결과를 확인한다.
다시exam12.html화면으로 돌아온 뒤,데이터 전송 1,데이터 전송 2,데이터 전송 3버튼을 클릭한다.
각 버튼을 클릭할 때마다#msg_area에는 해당 버튼의 데이터가이름:나이형태로 출력된다.// exam12.html AJAX 요청 출력결과 // 데이터 전송 1 클릭: 둘리:10 // 데이터 전송 2 클릭: 또치:11 // 데이터 전송 3 클릭: 도우너:12
then()방식에서는fetch()뒤에then()을 이어 붙여POST응답을JSON으로 변환하고, 그 결과를#msg_area에 출력한다.
exam12_1.html도AJAX출력 결과는 같다.
다만exam12.html은then()방식으로 응답을 처리하고,exam12_1.html은async/await방식으로 응답을 처리한다.
즉, 화면 결과는 같고 코드 작성 방식만 다르다.// exam12_1.html AJAX 요청 출력결과 // 데이터 전송 1 클릭: 둘리:10 // 데이터 전송 2 클릭: 또치:11 // 데이터 전송 3 클릭: 도우너:12
exam12_1.html에서는await fetch()로 응답을 기다리고,await response.json()으로JSON변환을 기다린 뒤 결과를 화면에 출력한다.
그래서 결과는exam12.html과 같지만, 요청 처리 흐름을 위에서 아래로 읽기 쉽게 작성한 차이가 있다.
이 결과에서 중요한 부분은 두 가지이다.
첫 번째는form태그 요청과AJAX요청의 처리 방식이 다르다는 점이다.
두 번째는then()방식과async/await방식 모두 최종AJAX결과는 같다는 점이다.
이 예제의 최종 결과물은form태그 요청이 브라우저 기본 전송 방식으로 처리되고,AJAX요청은 버튼 클릭에 따라JSON요청 본문을 서버로 보낸 뒤 응답을#msg_area에 출력하는 화면이다.
핵심 정리
이 예제의 핵심은
AJAX로JSON데이터를POST요청 본문에 담아 보내는 흐름이다.
버튼에는 전송할 값이data-*속성으로 들어 있고, 클릭 이벤트가 발생하면 그 값이 요청 데이터로 사용된다.
이 흐름은 아래처럼 정리할 수 있다.
form태그는 일반POST요청을 보내지만,AJAX처럼JSON요청을 보내는 방식은 아니다.form태그 요청은 기본적으로application/x-www-form-urlencoded형식으로 입력값을 전송한다.AJAX요청은 화면 이동 없이 서버와 데이터를 주고받을 수 있다.querySelectorAll("button")으로 버튼들을 선택한다.addEventListener("click", postData)로 버튼 클릭 이벤트를 연결한다.event.target은 실제 클릭된 버튼이다.event.target.id는 요청 경로를 만들 때 사용된다.getAttribute("data-pname")으로 이름 값을 꺼낸다.getAttribute("data-page")로 나이 값을 꺼낸다.JSON.stringify()로 객체를JSON문자열로 변환한다.AJAX요청에서Content-Type은application/json으로 설정한다.fetch()는POST요청을 서버로 보낸다.- 서버 응답은
response.json()으로 변환한다.- 응답 결과는
#msg_area에이름:나이형태로 출력한다.then()방식과async/await방식은 같은 결과를 만드는 다른 작성 방식이다.따라서 이 예제가 알려 주는 핵심은 이것이다.
AJAX로JSON데이터를 보낼 때는 클릭된 요소에서 값을 꺼내 객체를 만들고,JSON.stringify()로 문자열로 변환한 뒤Content-Type: application/json과 함께POST요청 본문에 담아 전송한다.
다음 예제에서는REST API요청을GET,POST,DELETE방식으로 나누어 처리하는 흐름을 확인한다.
이 예제는
AJAX로REST API에 요청을 보내고, 게시글 데이터를 조회, 등록, 삭제하는 흐름을 보여 준다.
앞의 예제에서는 버튼을 클릭했을 때JSON데이터를POST요청 본문으로 보내는 흐름을 확인했다.
이번 예제에서는 게시글 데이터에 대해GET,POST,DELETE요청을 나누어 실행한다.
REST API는 주소와 요청 방식을 함께 사용해서 자원을 다루는 방식이다.
여기서 자원은 게시글 데이터이다.
같은/boards주소를 사용하더라도 요청 방식이GET인지,POST인지,DELETE인지에 따라 서버에서 실행되는 동작이 달라진다.
exam13.html은then()방식으로 응답을 처리한다.
exam13_1.html은 같은 요청 흐름을async/await방식으로 처리한다.
두 파일 모두 버튼을 클릭하면fetch()요청이 실행되고, 결과는 개발자 도구Console에서 확인한다.
이 예제의 핵심은REST API요청에서 주소와 요청 방식을 함께 봐야 한다는 점이다.
GET /boards는 전체 조회이고,GET /boards/2는 단건 조회이며,POST /boards는 등록이고,DELETE /boards/5는 삭제이다.
예제 전체 코드
// exam13.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>ECMAScript6</title> </head> <body> <h1> AJAX 기술로 REST API 요청(CORS)</h1> <hr> <button onclick="getAll()">GET(ALL)</button> <button onclick="getOne()">GET(One)</button> <button onclick="postData()">POST</button> <button onclick="deleteData()">DELETE</button> <script> const ajaxrequest = { get(url) { return fetch(url); }, getOne(url) { return fetch(url); }, post(url, payload) { return fetch(url, { method: 'POST', headers: { 'content-Type': 'application/json' }, body: JSON.stringify(payload) }); }, delete(url) { return fetch(url, { method: 'DELETE' }); } } function getAll() { ajaxrequest.get('http://localhost:9000/boards') .then(response => { if (!response.ok) throw new Error(response.statusText); return response.json(); }) .then(boards => console.log(boards)) .catch(err => console.error(err)); } function getOne() { ajaxrequest.get('http://localhost:9000/boards/2') .then(response => { if (!response.ok) throw new Error(response.statusText); return response.json(); }) .then(board => console.log(board)) .catch(err => console.error(err)); } function postData() { ajaxrequest.post('http://localhost:9000/boards', { boardNo: 5, title: '꽃잎 한 장처럼', content: '수도생활 50년, 좋은 삶과 관계를 위한 통찰', writer: '이해인', regDate: '2020-05-25T09:00' }).then(response => { if (!response.ok) throw new Error(response.statusText); return response.text(); }) .then(msg => console.log(msg)) .catch(err => console.error(err)); } function deleteData() { ajaxrequest.delete('http://localhost:9000/boards/5') .then(response => { if (!response.ok) throw new Error(response.statusText); return response.text(); }) .then(msg => console.log(msg)) .catch(err => console.error(err)); } </script> </body> </html>// exam13_1.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>ECMAScript6</title> </head> <body> <h1> AJAX 기술로 REST API 요청(CORS)</h1> <hr> <button onclick="getAll()">GET(ALL)</button> <button onclick="getOne()">GET(One)</button> <button onclick="postData()">POST</button> <button onclick="deleteData()">DELETE</button> <script> // AJAX 요청을 관리하는 객체 const ajaxrequest = { async get(url) { const response = await fetch(url); return response; }, async post(url, payload) { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); return response; }, async delete(url) { const response = await fetch(url, { method: 'DELETE' }); return response; } } // 1. 전체 데이터 가져오기 async function getAll() { try { const response = await ajaxrequest.get('http://localhost:9000/boards'); if (!response.ok) throw new Error(`에러: ${response.statusText}`); const boards = await response.json(); console.log('전체 목록:', boards); } catch (err) { console.error('불러오기 실패:', err); } } // 2. 특정 데이터 가져오기 async function getOne() { try { const response = await ajaxrequest.get('http://localhost:9000/boards/2'); if (!response.ok) throw new Error(`에러: ${response.statusText}`); const board = await response.json(); console.log('상세 데이터:', board); } catch (err) { console.error('조회 실패:', err); } } // 3. 데이터 등록하기 async function postData() { const newBoard = { boardNo: 5, title: '꽃잎 한 장처럼', content: '수도생활 50년, 좋은 삶과 관계를 위한 통찰', writer: '이해인', regDate: '2020-05-25T09:00' }; try { const response = await ajaxrequest.post('http://localhost:9000/boards', newBoard); if (!response.ok) throw new Error(`에러: ${response.statusText}`); const msg = await response.text(); console.log('등록 성공:', msg); } catch (err) { console.error('등록 실패:', err); } } // 4. 데이터 삭제하기 async function deleteData() { try { const response = await ajaxrequest.delete('http://localhost:9000/boards/5'); if (!response.ok) throw new Error(`에러: ${response.statusText}`); const msg = await response.text(); console.log('삭제 성공:', msg); } catch (err) { console.error('삭제 실패:', err); } } </script> </body> </html>이 코드는 크게 두 파일로 나누어 볼 수 있다.
첫 번째exam13.html은then()방식으로REST API응답을 처리한다.
두 번째exam13_1.html은async/await방식으로 같은 요청을 처리한다.
두 파일 모두 화면에는 네 개의 버튼이 있다.
GET(ALL)버튼은 전체 게시글 목록을 조회한다.
GET(One)버튼은 게시글 하나를 조회한다.
POST버튼은 새 게시글을 등록한다.
DELETE버튼은 게시글 하나를 삭제한다.
두 예제는 코드 작성 방식은 다르지만, 버튼 클릭 →REST API요청 → 응답 확인 →Console출력 흐름은 같다.
이 예제에서 확인할 핵심
REST API는 주소와 요청 방식을 함께 사용해서 자원을 다룬다.GET /boards는 게시글 전체 목록을 조회한다.GET /boards/2는2번 게시글 하나를 조회한다.POST /boards는 새 게시글 데이터를 등록한다.DELETE /boards/5는5번 게시글을 삭제한다.ajaxrequest객체는fetch()요청을 메서드별로 묶어 관리한다.response.ok는 응답이 정상인지 확인한다.- 목록 조회와 단건 조회는
response.json()으로 응답 본문을 객체나 배열로 변환한다.- 등록과 삭제는
response.text()로 문자열 응답을 읽는다.exam13.html은then()방식으로 처리한다.exam13_1.html은async/await방식으로 처리한다.이 예제는 게시글 데이터를 대상으로
GET,POST,DELETE요청을 보내고, 요청 방식에 따라 서버 동작이 달라지는 흐름을 확인하는 예제이다.
REST API란 무엇인가
REST API는 서버의 자원을 주소로 표현하고, 요청 방식으로 동작을 구분하는 방식이다.
여기서 자원은 게시글 데이터이다.
게시글 전체 목록도 자원이고, 게시글 하나도 자원이다.
예를 들어/boards는 게시글 목록 자원을 의미한다.
/boards/2는 게시글 중에서2번 게시글 하나를 의미한다.
주소만 보면 비슷해 보일 수 있다.
하지만 요청 방식이 달라지면 의미도 달라진다.
GET은 조회할 때 사용하고,POST는 등록할 때 사용하며,DELETE는 삭제할 때 사용한다.
이 예제에서는 아래 요청을 사용한다.
GET http://localhost:9000/boardsGET http://localhost:9000/boards/2POST http://localhost:9000/boardsDELETE http://localhost:9000/boards/5
REST API에서는 주소만 보는 것이 아니라, 요청 방식까지 함께 봐야 어떤 동작인지 알 수 있다.
버튼은 각각 다른 요청 함수를 실행한다
// exam13.html <button onclick="getAll()">GET(ALL)</button> <button onclick="getOne()">GET(One)<</button> <button onclick="postData()">POST</button> <button onclick="deleteData()">DELETE</button>화면에는 네 개의 버튼이 있다.
각 버튼은onclick속성으로 실행할 함수를 연결한다.
GET(ALL)버튼을 클릭하면getAll()함수가 실행된다.
GET(One)버튼을 클릭하면getOne()함수가 실행된다.
POST버튼을 클릭하면postData()함수가 실행된다.
DELETE버튼을 클릭하면deleteData()함수가 실행된다.
즉, 버튼 하나가 하나의REST API요청 흐름과 연결되어 있다.
이 예제에서는 버튼 클릭이REST API요청을 시작하는 이벤트 역할을 한다.
ajaxrequest 객체는 요청 기능을 묶어 둔 객체이다
// exam13.html const ajaxrequest = { get(url) { return fetch(url); }, getOne(url) { return fetch(url); }, post(url, payload) { return fetch(url, { method: 'POST', headers: { 'content-Type': 'application/json' }, body: JSON.stringify(payload) }); }, delete(url) { return fetch(url, { method: 'DELETE' }); } }
ajaxrequest는AJAX요청을 메서드별로 묶어 둔 객체이다.
객체 안에는get(),getOne(),post(),delete()메서드가 들어 있다.
get()과getOne()은fetch(url)을 실행한다.
fetch()에서 요청 방식을 따로 지정하지 않으면 기본 요청 방식은GET이다.
그래서 전체 조회와 단건 조회는 별도로method를 지정하지 않아도GET요청이 된다.
post()는POST요청을 보내기 위해method: 'POST'를 지정한다.
그리고 등록할 게시글 데이터를payload로 받아JSON.stringify(payload)로JSON문자열로 바꾼 뒤body에 담는다.
delete()는 삭제 요청을 보내기 위해method: 'DELETE'를 지정한다.
ajaxrequest객체를 사용하면 요청 방식별fetch()코드를 한곳에 묶어서 관리할 수 있다.
GET 전체 조회 요청하기
// exam13.html function getAll() { ajaxrequest.get('http://localhost:9000/boards') .then(response => { if (!response.ok) throw new Error(response.statusText); return response.json(); }) .then(boards => console.log(boards)) .catch(err => console.error(err)); }
getAll()은 전체 게시글 목록을 조회하는 함수이다.
요청 주소는http://localhost:9000/boards이다.
이 요청은GET /boards요청이다.
서버는 게시글 전체 목록을 응답으로 돌려준다.
여러 게시글이 반환되므로 응답은 배열 형태로 볼 수 있다.
첫 번째then()에서는response.ok를 확인한다.
응답이 정상이 아니면 오류를 발생시킨다.
정상 응답이면response.json()을 호출해서 응답 본문을JSON데이터로 변환한다.
두 번째then()에서는 변환된 목록을boards로 받는다.
그리고console.log(boards)로 개발자 도구Console에 출력한다.
GET /boards요청은 게시글 전체 목록을 가져오고, 그 결과는JSON배열 형태로 확인할 수 있다.
GET 단건 조회 요청하기
// exam13.html function getOne() { ajaxrequest.get('http://localhost:9000/boards/2') .then(response => { if (!response.ok) throw new Error(response.statusText); return response.json(); }) .then(board => console.log(board)) .catch(err => console.error(err)); }
getOne()은 게시글 한 개를 조회하는 함수이다.
요청 주소는http://localhost:9000/boards/2이다.
이 요청은GET /boards/2요청이다.
여기서 마지막의2는 게시글 번호이다.
서버는2번 게시글 하나를 찾아 응답으로 돌려준다.
응답이 정상이라면response.json()으로 응답 본문을 객체로 변환한다.
그다음 변환된 게시글 객체를board로 받아Console에 출력한다.
전체 조회는 배열이 출력되고, 단건 조회는 게시글 객체 하나가 출력된다.
GET /boards/2요청은 경로에 들어간2번 게시글 하나를 조회하는 요청이다.
POST 등록 요청하기
// exam13.html function postData() { ajaxrequest.post('http://localhost:9000/boards', { boardNo: 5, title: '꽃잎 한 장처럼', content: '수도생활 50년, 좋은 삶과 관계를 위한 통찰', writer: '이해인', regDate: '2020-05-25T09:00' }).then(response => { if (!response.ok) throw new Error(response.statusText); return response.text(); }) .then(msg => console.log(msg)) .catch(err => console.error(err)); }
postData()는 새 게시글을 등록하는 함수이다.
요청 주소는http://localhost:9000/boards이다.
요청 방식은POST이다.
등록할 게시글 데이터는 객체 형태로 작성되어 있다.
이 객체에는 게시글 번호, 제목, 내용, 작성자, 등록일이 들어 있다.
ajaxrequest.post()내부에서는 이 객체를JSON.stringify(payload)로JSON문자열로 변환한다.
그리고 그 문자열을 요청 본문body에 담아 서버로 보낸다.
등록 요청의 응답은 문자열 메시지로 처리한다.
그래서response.json()이 아니라response.text()를 사용한다.
그다음msg를Console에 출력한다.
POST /boards요청은 새 게시글 데이터를JSON요청 본문에 담아 서버에 등록하는 요청이다.
DELETE 삭제 요청하기
// exam13.html function deleteData() { ajaxrequest.delete('http://localhost:9000/boards/5') .then(response => { if (!response.ok) throw new Error(response.statusText); return response.text(); }) .then(msg => console.log(msg)) .catch(err => console.error(err)); }
deleteData()는 게시글 하나를 삭제하는 함수이다.
요청 주소는http://localhost:9000/boards/5이다.
요청 방식은DELETE이다.
여기서 마지막의5는 삭제할 게시글 번호이다.
즉,DELETE /boards/5는5번 게시글을 삭제하겠다는 요청이다.
삭제 요청의 응답도 문자열 메시지로 처리한다.
그래서response.text()로 응답 본문을 문자열로 읽고, 그 메시지를Console에 출력한다.
DELETE /boards/5요청은 경로값5에 해당하는 게시글 삭제 요청을 보낸다.
async await 방식으로 REST API 요청하기
// exam13_1.html async function getAll() { try { const response = await ajaxrequest.get('http://localhost:9000/boards'); if (!response.ok) throw new Error(`에러: ${response.statusText}`); const boards = await response.json(); console.log('전체 목록:', boards); } catch (err) { console.error('불러오기 실패:', err); } }
exam13_1.html은async/await방식으로 같은REST API요청을 처리한다.
요청 대상과 결과는exam13.html과 같다.
차이는Promise응답을 처리하는 문법이다.
await ajaxrequest.get(...)은 요청 응답이 올 때까지 기다린다.
응답이 오면response.ok로 정상 응답인지 확인한다.
정상 응답이면await response.json()으로 응답 본문 변환을 기다린다.
try안에는 정상 처리 흐름을 작성한다.
catch안에는 오류가 발생했을 때의 흐름을 작성한다.
이 방식은then()을 계속 이어 붙이는 방식보다 요청 → 응답 확인 → 응답 변환 → 출력 흐름이 위에서 아래로 보인다.
async/await방식은REST API요청 흐름을 순서대로 읽기 쉽게 작성할 수 있게 해 준다.
then 방식과 async await 방식 비교하기
exam13.html과exam13_1.html은 같은 버튼과 같은 요청 주소를 사용한다.
두 파일 모두 전체 조회, 단건 조회, 등록, 삭제 요청을 보낸다.
차이는 응답 처리 방식이다.
exam13.html은then()과catch()를 이어 붙여 처리한다.
exam13_1.html은async/await와try/catch를 사용해 처리한다.
정리하면 아래처럼 볼 수 있다.
then()방식은fetch()결과 뒤에.then()을 이어 붙인다.async/await방식은await fetch()처럼 응답을 기다리는 코드로 작성한다.- 두 방식 모두
response.ok로 응답 상태를 확인한다.- 조회 요청은
response.json()으로 응답을 변환한다.- 등록과 삭제 요청은
response.text()로 문자열 응답을 읽는다.- 두 방식 모두 결과는 개발자 도구
Console에서 확인한다.
then()과async/await는 결과가 다른 방식이 아니라, 같은REST API요청 흐름을 다른 문법으로 작성하는 방식이다.
실제 출력 결과 확인하기
이 예제의 결과는 브라우저 화면이 아니라 개발자 도구
Console에서 확인한다.
버튼을 클릭하면 요청 결과가Console에 출력된다.
버튼별 출력 흐름은 아래처럼 볼 수 있다.// 출력결과 예시 // GET(ALL) 클릭: 게시글 전체 목록 배열 출력 // GET(One) 클릭: 2번 게시글 객체 출력 // POST 클릭: 등록 성공 문자열 출력 // DELETE 클릭: 삭제 성공 문자열 출력조회 요청은
JSON데이터로 응답을 받는다.
그래서 전체 조회는 배열 형태로 출력되고, 단건 조회는 객체 형태로 출력된다.
등록과 삭제 요청은 문자열 메시지로 응답을 받는다.
그래서POST와DELETE결과는 문자열 메시지로 출력된다.
이 예제에서는 요청 방식과 요청 주소에 따라Console에 출력되는 응답 형태가 달라진다.
결과물 뽑기
이 예제의 결과물은 브라우저 화면보다 개발자 도구
Console을 중심으로 확인한다.
화면에는 버튼만 있고, 요청 결과는 모두Console에 출력된다.
먼저exam13.html을 브라우저에서 실행한다.
GET(ALL),GET(One),POST,DELETE버튼을 순서대로 클릭한다.
각 버튼을 클릭하면then()방식으로 요청이 처리되고, 결과가Console에 출력된다.// exam13.html 출력결과 // GET(ALL): 게시글 전체 목록 배열 출력 // GET(One): 2번 게시글 객체 출력 // POST: 등록 성공 문자열 출력 // DELETE: 삭제 성공 문자열 출력
then()방식에서는 각 요청 뒤에then()을 이어 붙여 응답을 변환하고, 결과를Console에 출력한다.
다음으로exam13_1.html을 브라우저에서 실행한다.
같은 버튼 네 개를 클릭한다.
이번에는async/await방식으로 요청이 처리되고, 결과가Console에 출력된다.// exam13_1.html 출력결과 // GET(ALL): 전체 목록 출력 // GET(One): 상세 데이터 출력 // POST: 등록 성공 문자열 출력 // DELETE: 삭제 성공 문자열 출력
async/await방식에서는await로 응답을 기다린 뒤, 조회 요청은response.json(), 등록과 삭제 요청은response.text()로 응답 본문을 읽는다.
이 결과에서 중요한 부분은 네 버튼이 각각 다른REST API요청을 실행한다는 점이다.
GET(ALL)은 전체 조회,GET(One)은 단건 조회,POST는 등록,DELETE는 삭제 요청이다.
이 예제의 최종 결과물은 버튼 클릭에 따라GET,POST,DELETE요청이 실행되고, 각 요청의 응답이 개발자 도구Console에 출력되는 화면이다.
핵심 정리
이 예제의 핵심은
AJAX로REST API요청을 보내는 흐름이다.
게시글 데이터를 대상으로 전체 조회, 단건 조회, 등록, 삭제 요청을 각각 실행한다.
이 흐름은 아래처럼 정리할 수 있다.
REST API는 주소와 요청 방식을 함께 사용해서 자원을 다룬다.GET /boards는 게시글 전체 목록을 조회한다.GET /boards/2는2번 게시글 하나를 조회한다.POST /boards는 새 게시글을 등록한다.DELETE /boards/5는5번 게시글을 삭제한다.ajaxrequest객체는 요청 방식을 메서드로 묶어 관리한다.GET요청은 기본fetch(url)로 보낼 수 있다.POST요청은method: 'POST'와 요청 본문body가 필요하다.DELETE요청은method: 'DELETE'를 지정한다.- 조회 응답은
response.json()으로 변환한다.- 등록과 삭제 응답은
response.text()로 읽는다.then()방식과async/await방식은 같은 결과를 만드는 다른 작성 방식이다.따라서 이 예제가 알려 주는 핵심은 이것이다.
AJAX로REST API를 호출할 때는 요청 주소와 요청 방식을 함께 정해야 하며, 응답 형태에 따라response.json()또는response.text()로 다르게 읽어야 한다.
다음 예제에서는fetch()로 이미지 파일을 받아Blob형태로 처리하고 화면에 출력하는 흐름을 확인한다.
이 예제는
AJAX로 이미지 파일을 요청하고, 응답으로 받은 이미지 데이터를 브라우저 화면에 출력하는 흐름을 보여 준다.
앞의 예제에서는JSON데이터나 문자열 데이터를 요청해서Console이나 화면에 출력했다.
이번에는 텍스트가 아니라 실제 이미지 파일을 받아서 화면에 붙이는 흐름을 확인한다.
이미지 파일은JSON처럼response.json()으로 읽지 않는다.
문자열처럼response.text()로 읽지도 않는다.
이미지 같은 파일 데이터는response.blob()으로 읽는다.
exam14.html은then()방식으로 이미지를 처리한다.
exam14_1.html은 같은 흐름을async/await방식으로 처리한다.
두 파일 모두 제목을 클릭하면 이미지 요청이 발생하고, 정상 응답이면 받아온 이미지를img태그로 만들어 화면에 추가한다.
다만 현재 결과물에서는1.jpg만 정상적으로 출력되고, 이후2.jpg,3.jpg,4.jpg요청은 파일을 찾지 못해404오류가 발생한다.
이 예제의 핵심은 이미지 파일 응답을Blob으로 받은 뒤URL.createObjectURL()로 브라우저가 사용할 수 있는 임시 주소를 만들어img.src에 넣는 흐름과, 파일이 없을 때 오류 처리 흐름을 함께 확인하는 것이다.
예제 전체 코드
// exam14.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Ajax테스트</title> </head> <body> <h2 onclick="getImage();">이 행을 클릭해요. Ajax로 이미지를 요청하고 출력해요....</h2> <script> let imgname = 0; function getImage() { imgname = imgname == 10 ? 1 : imgname+1; fetch(`../../images/${imgname}.jpg`) .then(response => { if (response.status == 200) return response.blob(); else throw new Error("오류발생"); }) .then(blobdata => { const img = document.createElement('img'); img.width=100; img.height=100; img.src = URL.createObjectURL(blobdata); document.body.appendChild(img); }); } </script> </body> </html>// exam14_1.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Ajax테스트</title> </head> <body> <h2 onclick="getImage();">이 행을 클릭해요. Ajax로 이미지를 요청하고 출력해요....</h2> <script> let imgname = 0; async function getImage() { // 이미지 번호 순환 (1~10) imgname = imgname === 10 ? 1 : imgname + 1; try { // 1. 이미지 데이터 요청 const response = await fetch(`../../images/${imgname}.jpg`); // 2. 응답 상태 확인 if (!response.ok) { throw new Error("이미지를 불러오는 중 오류가 발생했습니다."); } // 3. Blob 데이터로 변환 대기 const actualBlob = await response.blob(); // 4. 이미지 요소 생성 및 설정 const img = document.createElement('img'); img.width = 100; img.height = 100; // Blob URL을 만들어 img.src에 연결한다. img.src = URL.createObjectURL(actualBlob); // 5. 화면에 추가 document.body.appendChild(img); } catch (err) { console.error("잡았다!! : " + err.message); } } </script> </body> </html>이 코드는 크게 두 파일로 나누어 볼 수 있다.
첫 번째exam14.html은then()방식으로 이미지 요청 결과를 처리한다.
두 번째exam14_1.html은async/await방식으로 같은 이미지 요청을 처리한다.
두 파일 모두 화면에는 클릭 가능한h2제목이 하나 있다.
이 제목을 클릭하면getImage()함수가 실행된다.
그리고../../images/1.jpg,../../images/2.jpg처럼 이미지 파일을 하나씩 요청한다.
코드상으로는 응답이 정상일 때 새img태그를 만들어 화면에 추가하도록 작성되어 있다.
하지만 현재 결과물에서는1.jpg만 정상 응답이고, 이후 이미지 파일은 존재하지 않아 오류 흐름으로 넘어간다.
두 예제는 코드 작성 방식은 다르지만, 클릭 → 이미지 요청 → 응답 상태 확인 → 정상 응답이면Blob변환 → 이미지 출력, 실패 응답이면 오류 처리라는 흐름은 같다.
이 예제에서 확인할 핵심
- 제목을 클릭하면
getImage()함수가 실행된다.imgname변수는 현재 요청할 이미지 번호를 관리한다.- 이미지 번호는
1부터10까지 순서대로 바뀌고,10다음에는 다시1로 돌아가도록 작성되어 있다.fetch()는 이미지 파일에도 사용할 수 있다.- 이미지 응답은
response.blob()으로 읽는다.Blob은 파일처럼 다룰 수 있는 이진 데이터 덩어리이다.URL.createObjectURL()은Blob데이터를 브라우저가 사용할 수 있는 임시 주소로 바꾼다.document.createElement('img')로 이미지 태그를 만든다.img.src에Blob임시 주소를 넣으면 화면에 이미지가 보인다.- 정상 응답이면
appendChild()로 이미지를 화면에 추가한다.- 현재 결과물에서는
1.jpg만 정상 출력되고, 이후 이미지 요청은404오류가 발생한다.exam14.html은then()방식이다.exam14_1.html은async/await방식이다.이 예제는
fetch()로 이미지 파일을 요청한 뒤, 정상 응답은Blob데이터로 화면에 출력하고, 실패 응답은 오류 흐름으로 처리하는 예제이다.
클릭하면 getImage 함수가 실행된다
// exam14.html <h2 onclick="getImage();">이 행을 클릭해요. Ajax로 이미지를 요청하고 출력해요....</h2>이 예제에서는
h2태그를 클릭하면getImage()함수가 실행된다.
즉, 버튼이 아니라 제목 문장을 클릭해서 이미지 요청을 시작하는 구조이다.
onclick은 클릭 이벤트가 발생했을 때 실행할 코드를 연결하는 속성이다.
여기서는getImage()를 연결했으므로, 사용자가 제목을 한 번 클릭할 때마다 이미지 요청 함수가 한 번 실행된다.
이 예제에서 이미지 요청의 시작점은h2제목 클릭이다.
imgname 변수는 요청할 이미지 번호를 관리한다
// exam14.html let imgname = 0; function getImage() { imgname = imgname == 10 ? 1 : imgname+1; }
imgname은 현재 몇 번 이미지를 요청할지 저장하는 변수이다.
처음에는0으로 시작한다.
getImage()가 실행되면imgname값이 바뀐다.
imgname == 10 ? 1 : imgname+1은 삼항 연산자를 사용한 코드이다.
뜻은 아래와 같다.
- 현재 값이
10이면 다시1로 바꾼다.- 현재 값이
10이 아니면1증가시킨다.그래서 클릭할 때마다 이미지 번호는 아래처럼 변한다.
// 이미지 번호 변화 // 1 → 2 → 3 → 4 → ... → 10 → 1 → 2 ...즉, 코드상으로는
1.jpg부터10.jpg까지 순서대로 요청하고, 그다음에는 다시1.jpg부터 반복하도록 되어 있다.
하지만 현재 결과물에서는1.jpg만 정상적으로 존재한다.
그래서 첫 번째 클릭은 성공하지만, 두 번째 클릭부터는 존재하지 않는 이미지 파일을 요청해서404오류가 발생한다.
imgname은 클릭할 때마다 다음 이미지 번호로 바뀌지만, 요청한 파일이 실제로 존재해야 화면에 출력할 수 있다.
fetch로 이미지 파일을 요청한다
// exam14.html fetch(`../../images/${imgname}.jpg`)이 코드는 현재
imgname값에 맞는 이미지 파일을 요청한다.
예를 들어imgname이1이면../../images/1.jpg를 요청한다.
imgname이2이면../../images/2.jpg를 요청한다.
문자열 안에${imgname}이 들어간 것은 템플릿 리터럴이다.
이 방식은 변수 값을 문자열 안에 쉽게 끼워 넣을 수 있게 해 준다.
이 예제에서는../../images/1.jpg,../../images/2.jpg처럼 상대 경로에 있는 이미지 파일을 하나씩 요청한다.
정상적으로 존재하는 파일이면 응답 상태가 성공이 되고, 존재하지 않는 파일이면404응답이 된다.
fetch()는 이미지 파일 같은 리소스도 요청할 수 있지만, 요청한 파일이 없으면404오류 응답을 받는다.
이미지 응답은 Blob으로 읽는다
// exam14.html .then(response => { if (response.status == 200) return response.blob(); else throw new Error("오류발생"); })이미지 파일은
JSON데이터가 아니다.
그래서response.json()으로 읽지 않는다.
문자열도 아니므로response.text()로 읽지 않는다.
이미지처럼 파일 형태의 응답은response.blob()으로 읽는다.
여기서Blob은 바이너리 파일 데이터를 담는 객체라고 이해하면 된다.
쉽게 말하면 이미지, 오디오, 동영상 같은 파일 데이터를JavaScript에서 다룰 수 있게 감싼 형태이다.
먼저response.status == 200인지 확인한다.
200은 요청이 정상적으로 성공했다는 뜻이다.
정상 응답이면response.blob()을 반환한다.
그렇지 않으면throw new Error("오류발생")으로 오류를 발생시킨다.
현재 결과물에서는 첫 번째 클릭의1.jpg요청은 정상 응답이다.
그래서response.blob()으로 넘어가 이미지가 화면에 출력된다.
하지만 두 번째 클릭부터는2.jpg요청이404가 되므로throw new Error("오류발생")이 실행된다.
이미지 파일 응답은 성공 상태일 때만Blob으로 변환할 수 있고, 실패 상태라면 오류 처리 흐름으로 넘어가야 한다.
URL.createObjectURL로 Blob을 이미지 주소로 바꾼다
// exam14.html .then(blobdata => { const img = document.createElement('img'); img.width=100; img.height=100; img.src = URL.createObjectURL(blobdata); document.body.appendChild(img); });
response.blob()으로 얻은blobdata는 파일 데이터이다.
하지만 이 데이터를 바로img.src에 넣을 수는 없다.
img.src에는 브라우저가 읽을 수 있는 주소 형태가 들어가야 한다.
이때 사용하는 것이URL.createObjectURL(blobdata)이다.
이 함수는Blob데이터를 가리키는 임시 주소를 만들어 준다.
브라우저는 이 임시 주소를 통해 메모리에 있는 파일 데이터를 이미지처럼 읽을 수 있다.
그다음document.createElement('img')로 새img태그를 만든다.
img.width = 100,img.height = 100으로 이미지 크기를 지정한다.
그리고img.src에URL.createObjectURL(blobdata)가 만든 주소를 넣는다.
마지막으로document.body.appendChild(img)로 화면에 추가한다.
이 흐름은 이미지 요청이 성공했을 때만 실행된다.
요청한 이미지 파일이 없어서 오류가 발생하면 이 단계까지 오지 못한다.
Blob데이터를 화면에 출력하려면,URL.createObjectURL()로 임시 주소를 만든 뒤 그 주소를img.src에 넣어야 한다.
appendChild는 정상 응답일 때 이미지를 화면에 추가한다
// exam14.html document.body.appendChild(img);이 코드는 새로 만든 이미지를
body의 마지막 자식으로 추가한다.
즉, 이미지 요청이 성공하고img태그가 만들어진 경우에만 화면에 이미지가 추가된다.
코드 구조만 보면 정상 응답이 여러 번 오면 이미지가 여러 장 추가될 수 있다.
하지만 현재 결과물에서는 첫 번째 이미지 요청만 정상이다.
그래서 실제 화면에는 첫 번째 이미지 한 장만 출력된다.
두 번째 클릭부터는2.jpg,3.jpg,4.jpg처럼 파일을 요청하지만, 해당 파일을 찾지 못해404가 발생한다.
이 경우Blob변환과appendChild()단계로 가지 못한다.
appendChild()는 이미지를 누적해서 추가할 수 있는 코드이지만, 현재 결과물에서는1.jpg만 정상 응답이므로 이미지 한 장만 화면에 보인다.
async await 방식으로 이미지 요청 처리하기
// exam14_1.html async function getImage() { imgname = imgname === 10 ? 1 : imgname + 1; try { const response = await fetch(`../../images/${imgname}.jpg`); if (!response.ok) { throw new Error("이미지를 불러오는 중 오류가 발생했습니다."); } const actualBlob = await response.blob(); const img = document.createElement('img'); img.width = 100; img.height = 100; img.src = URL.createObjectURL(actualBlob); document.body.appendChild(img); } catch (err) { console.error("잡았다!! : " + err.message); } }
exam14_1.html은 같은 작업을async/await방식으로 처리한다.
요청 대상도 같고, 화면 출력 방식도 같다.
차이는 응답을 처리하는 문법이다.
await fetch(...)는 이미지 요청이 끝날 때까지 기다린다.
응답이 오면response.ok로 정상 응답인지 확인한다.
정상 응답이 아니면throw new Error(...)로 오류를 발생시킨다.
정상 응답이면await response.blob()으로Blob변환이 끝날 때까지 기다린다.
그다음 새img태그를 만들고,URL.createObjectURL(actualBlob)로 만든 임시 주소를img.src에 넣는다.
마지막으로appendChild()로 화면에 추가한다.
오류가 발생하면catch구간에서"잡았다!!"와 함께 오류 메시지를Console에 출력한다.
현재 결과물에서는2.jpg이후 요청이 실패하므로catch에서 오류 메시지가 출력된다.
async/await방식은 이미지 요청, 응답 확인,Blob변환, 화면 출력, 오류 처리를 위에서 아래로 읽기 쉽게 작성한다.
then 방식과 async await 방식 비교하기
exam14.html과exam14_1.html은 같은 일을 한다.
둘 다 제목을 클릭하면 이미지 파일을 요청하고, 정상 응답이면 받아온 이미지를 화면에 추가한다.
차이는 응답과 오류를 처리하는 방식이다.
exam14.html은then()방식으로 처리한다.
exam14_1.html은async/await와try/catch를 사용해 처리한다.
정리하면 아래처럼 볼 수 있다.
then()방식은fetch()뒤에.then()을 이어 붙여 처리한다.async/await방식은await fetch()로 응답을 기다린다.- 두 방식 모두 이미지 번호를
1~10사이에서 순환시키도록 작성되어 있다.- 두 방식 모두 이미지 응답이 정상이면
response.blob()으로 읽는다.- 두 방식 모두
URL.createObjectURL()로Blob임시 주소를 만든다.- 두 방식 모두 정상 응답이면
img태그를 만들어 화면에 추가한다.exam14.html은 오류가Uncaught (in promise)형태로 보인다.exam14_1.html은catch에서 오류를 잡아Console에 출력한다.두 방식은 정상 응답 처리 결과는 같지만, 오류가 발생했을 때 보이는 처리 방식이 다르다.
실제 출력 결과 확인하기
이 예제에서는 제목을 클릭할 때마다 이미지 번호가 하나씩 증가한다.
첫 번째 클릭에서는imgname이1이 되어../../images/1.jpg를 요청한다.
현재 결과물에서는1.jpg가 정상적으로 존재하기 때문에 화면에 이미지가 한 장 출력된다.
두 번째 클릭부터는imgname이2,3,4처럼 증가한다.
그래서../../images/2.jpg,../../images/3.jpg,../../images/4.jpg요청이 차례대로 발생한다.
하지만 현재 실행 결과에서는 해당 이미지 파일을 찾지 못해404오류가 발생한다.
exam14.html에서는response.status == 200이 아니면throw new Error("오류발생")이 실행된다.
그래서 개발자 도구Console에Uncaught (in promise) Error: 오류발생이 출력된다.
exam14_1.html에서는response.ok가false이면throw new Error("이미지를 불러오는 중 오류가 발생했습니다.")가 실행된다.
이 오류는catch에서 잡히기 때문에Console에잡았다!! : 이미지를 불러오는 중 오류가 발생했습니다.가 출력된다.
현재 결과물의 핵심은 이미지가 계속 누적되는 것이 아니라, 첫 번째 이미지만 정상 출력되고 이후 이미지 요청은404오류로 실패한다는 점이다.
결과물 뽑기
이 예제의 결과물은 브라우저 화면과 개발자 도구
Console을 함께 확인한다.
화면에서는 첫 번째 클릭으로 이미지 한 장이 출력되는지 확인한다.
Console에서는 이후 클릭에서 이미지 파일을 찾지 못해404오류가 발생하는지 확인한다.
먼저exam14.html을 브라우저에서 실행한다.
제목을 클릭하면 첫 번째 이미지인1.jpg가 화면에 출력된다.
다시 클릭하면2.jpg요청이 발생하지만, 현재 결과물에서는 해당 파일을 찾지 못해404오류가 발생한다.// exam14.html 출력결과 // 첫 번째 클릭: 1.jpg 정상 출력 // 두 번째 클릭: 2.jpg 요청 실패 // Console: GET http://localhost:9000/images/2.jpg 404 (Not Found) // Console: Uncaught (in promise) Error: 오류발생
then()방식에서는 응답 상태가200이면response.blob()으로 이미지 데이터를 읽고 화면에 출력한다.
하지만 응답 상태가200이 아니면throw new Error("오류발생")이 실행되어Console에 오류가 출력된다.
다음으로exam14_1.html을 브라우저에서 실행한다.
첫 번째 클릭에서는1.jpg가 화면에 출력된다.
이후 클릭에서는2.jpg,3.jpg,4.jpg요청이 발생하지만, 현재 결과물에서는 해당 파일을 찾지 못해404오류가 발생한다.// exam14_1.html 출력결과 // 첫 번째 클릭: 1.jpg 정상 출력 // 이후 클릭: 2.jpg, 3.jpg, 4.jpg 요청 실패 // Console: GET http://localhost:9000/images/2.jpg 404 (Not Found) // Console: 잡았다!! : 이미지를 불러오는 중 오류가 발생했습니다.
async/await방식에서는await fetch()로 이미지 요청 결과를 기다린다.
응답이 정상이 아니면throw new Error(...)가 실행되고, 이 오류를catch에서 받아Console에 출력한다.
이 결과에서 중요한 부분은 두 가지이다.
첫 번째는1.jpg는 정상적으로Blob으로 변환되어 화면에 출력된다는 점이다.
두 번째는 이후 이미지 파일이 없으면response.blob()까지 가지 못하고 오류 흐름으로 넘어간다는 점이다.
이 예제의 최종 결과물은 첫 번째 이미지는 정상 출력되고, 이후 존재하지 않는 이미지 요청은404오류로 처리되는 화면이다.
핵심 정리
이 예제의 핵심은
AJAX로 이미지 파일을 요청하고 화면에 출력하는 흐름이다.
텍스트 데이터나JSON과 달리 이미지 응답은Blob으로 읽어야 한다.
이 흐름은 아래처럼 정리할 수 있다.
- 제목을 클릭하면
getImage()함수가 실행된다.imgname은 현재 요청할 이미지 번호를 관리한다.- 이미지 번호는
1~10까지 순환하도록 작성되어 있다.fetch()로 이미지 파일을 요청한다.- 응답이 정상이면
response.blob()으로 파일 데이터를 읽는다.Blob은 파일 같은 이진 데이터를 담는 객체이다.URL.createObjectURL()은Blob데이터를 브라우저가 사용할 수 있는 임시 주소로 바꾼다.document.createElement('img')로 이미지 태그를 만든다.img.src에 임시 주소를 넣는다.appendChild()로 이미지를 화면에 추가한다.- 현재 결과물에서는
1.jpg만 정상 출력되고, 이후 이미지 요청은404오류가 발생한다.then()방식은 오류가Uncaught (in promise)로 보인다.async/await방식은catch에서 오류를 잡아Console에 출력한다.따라서 이 예제가 알려 주는 핵심은 이것이다.
이미지 파일을AJAX로 받아 화면에 출력하려면, 응답을response.blob()으로 읽고URL.createObjectURL()로 임시 주소를 만든 뒤img.src에 연결해야 한다. 다만 요청한 이미지 파일이 존재하지 않으면404오류가 발생하고, 코드의 오류 처리 흐름으로 넘어간다.
다음 예제에서는 외부Open API를 사용해서 박스오피스 데이터와 영화 이미지를 함께 요청하는 흐름을 확인한다.
이 예제는 외부
Open API를 사용해서 일별 박스오피스 데이터를 가져오고, 각 영화명으로 이미지 검색 요청을 다시 보내 영화 썸네일과 함께 화면에 출력하는 흐름을 보여 준다.
앞의 예제에서는 같은 서버나 로컬 이미지 파일을 요청했다.
이번에는 영화진흥위원회 박스오피스API와 카카오 이미지 검색API를 함께 사용한다.
이 예제의 요청 흐름은 한 번으로 끝나지 않는다.
먼저 영화진흥위원회API에 날짜별 박스오피스 목록을 요청한다.
그다음 박스오피스 목록에 들어 있는 영화명을 이용해서 카카오 이미지 검색API에 다시 요청을 보낸다.
즉, 첫 번째 요청으로 영화 목록을 받고, 두 번째 요청들로 영화 썸네일 이미지를 가져오는 구조이다.
이 예제의 핵심은 하나의AJAX요청 결과를 이용해서 여러 개의 추가 비동기 요청을 만들고, 그 결과를 테이블에 출력하는 흐름이다.
예제 전체 코드
// exam15.html <!doctype html> <html lang="ko"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" > <style> nav, section { margin : 10px; } </style> <title>Hello, world!</title> </head> <body> <header class="jumbotron text-center"> <h1>일별 박스 오피스</h1> <p>영화관 입장권 통합 전산망 오픈 API 를 AJAX 기술로 활용합니다.</p> </header> <nav> <button type="button" class="btn btn-outline-primary" onclick="displayMovieRanking(1)">1일전</button> <button type="button" class="btn btn-outline-info" onclick="displayMovieRanking(2)">2일전</button> <button type="button" class="btn btn-outline-success" onclick="displayMovieRanking(3)">3일전</button> </nav> <section> <table class="table table-hover"> <thead> <tr> <th scope="col">영화썸네일</th> <th scope="col">영화명</th> <th scope="col">해당일 관객수</th> <th scope="col">누적 관객수</th> <th scope="col">스크린수</th> </tr> </thead> <tbody> </tbody> </table> </section> <script> function getFormatDate(date) { const year = date.getFullYear(); let month = (1 + date.getMonth()); month = month >= 10 ? month : '0' + month; let day = date.getDate(); day = day >= 10 ? day : '0' + day; return "" + year + month + day; } function displayMovieRanking(n) { const today = new Date(); const d = today.getTime() - (n * 24 * 60 * 60 * 1000); let targetday = new Date(d); const targetDt = getFormatDate(targetday); let thumbnail = []; let movieNm = []; let audiCnt = []; let audiAcc = []; let scrnCnt = []; let url = 'http://www.kobis.or.kr/kobisopenapi/webservice/rest/boxoffice/searchDailyBoxOfficeList.json?key=발급받은_영화진흥위원회_API_KEY&targetDt=' + targetDt; const dom = document.getElementsByTagName('tbody')[0]; let content = ""; fetch(url) .then(response => { return response.json(); }) .then(jsonObj => { for (let i = 0; i < 10; i++) { movieNm[i] = jsonObj.boxOfficeResult.dailyBoxOfficeList[i].movieNm; audiCnt[i] = jsonObj.boxOfficeResult.dailyBoxOfficeList[i].audiCnt; audiAcc[i] = jsonObj.boxOfficeResult.dailyBoxOfficeList[i].audiAcc; scrnCnt[i] = jsonObj.boxOfficeResult.dailyBoxOfficeList[i].scrnCnt; } return movieNm; }) .then(mNm => { dom.innerHTML = ""; for (let i = 0; i < 10; i++) { fetch('https://dapi.kakao.com/v2/search/image?query=' + encodeURIComponent(mNm[i]) + '&sort=accuracy& page=1&size=5', { headers: { "Authorization" : "KakaoAK 발급받은_카카오_REST_API_KEY" } }) .then(response => { return response.json(); }) .then(jsonObj => { content = "<tr><td scope='row'><img src='" + jsonObj.documents[0].thumbnail_url + "' width='50'>" + "</td><td>" + movieNm[i] + "</td><td>" + audiCnt[i].toLocaleString() + "</td><td>" + audiAcc[i].toLocaleString() + "</td><td>" + scrnCnt[i] + "</td></tr>"; dom.innerHTML += content; }); } }); } </script> </body> </html>// exam15_1.html <!doctype html> <html lang="ko"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" > <style> nav, section { margin : 10px; } </style> <title>Hello, world!</title> </head> <body> <header class="jumbotron text-center"> <h1>일별 박스 오피스</h1> <p>영화관 입장권 통합 전산망 오픈 API 를 AJAX 기술로 활용합니다.</p> </header> <nav> <button type="button" class="btn btn-outline-primary" onclick="displayMovieRanking(1)">1일전</button> <button type="button" class="btn btn-outline-info" onclick="displayMovieRanking(2)">2일전</button> <button type="button" class="btn btn-outline-success" onclick="displayMovieRanking(3)">3일전</button> </nav> <section> <table class="table table-hover"> <thead> <tr> <th scope="col">영화썸네일</th> <th scope="col">영화명</th> <th scope="col">해당일 관객수</th> <th scope="col">누적 관객수</th> <th scope="col">스크린수</th> </tr> </thead> <tbody> </tbody> </table> </section> <script> function getFormatDate(date) { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return "" + year + month + day; } function displayMovieRanking(n) { const today = new Date(); const d = today.getTime() - (n * 24 * 60 * 60 * 1000); const targetDt = getFormatDate(new Date(d)); const url = 'http://www.kobis.or.kr/kobisopenapi/webservice/rest/boxoffice/searchDailyBoxOfficeList.json?key=발급받은_영화진흥위원회_API_KEY&targetDt=' + targetDt; const dom = document.getElementsByTagName('tbody')[0]; fetch(url) .then(response => response.json()) .then(jsonObj => { const movieList = jsonObj.boxOfficeResult.dailyBoxOfficeList; const imagePromises = movieList.map((movie, index) => { return fetch('https://dapi.kakao.com/v2/search/image?query=' + encodeURIComponent(movie.movieNm) + '&sort=accuracy&size=1', { headers: { "Authorization": "KakaoAK 발급받은_카카오_REST_API_KEY" } }) .then(res => res.json()) .then(imgData => { const thumb = imgData.documents.length > 0 ? imgData.documents[0].thumbnail_url : 'https://via.placeholder.com/50'; return { name: movie.movieNm, audiCnt: movie.audiCnt, audiAcc: movie.audiAcc, scrnCnt: movie.scrnCnt, img: thumb }; }); }); return Promise.all(imagePromises); }) .then(finalData => { let content = ""; finalData.forEach(movie => { content += "<tr><td scope='row'><img src='" + movie.img + "' width='50'>" + "</td><td>" + movie.name + "</td><td>" + Number(movie.audiCnt).toLocaleString() + "</td><td>" + Number(movie.audiAcc).toLocaleString() + "</td><td>" + movie.scrnCnt + "</td></tr>"; }); dom.innerHTML = content; }) .catch(err => console.error("데이터 로드 실패:", err)); } </script> </body> </html>// exam15_2.html <!doctype html> <html lang="ko"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" > <style> nav, section { margin : 10px; } </style> <title>Hello, world!</title> </head> <body> <header class="jumbotron text-center"> <h1>일별 박스 오피스</h1> <p>영화관 입장권 통합 전산망 오픈 API 를 AJAX 기술로 활용합니다.</p> </header> <nav> <button type="button" class="btn btn-outline-primary" onclick="displayMovieRanking(1)">1일전</button> <button type="button" class="btn btn-outline-info" onclick="displayMovieRanking(2)">2일전</button> <button type="button" class="btn btn-outline-success" onclick="displayMovieRanking(3)">3일전</button> </nav> <section> <table class="table table-hover"> <thead> <tr> <th scope="col">영화썸네일</th> <th scope="col">영화명</th> <th scope="col">해당일 관객수</th> <th scope="col">누적 관객수</th> <th scope="col">스크린수</th> </tr> </thead> <tbody> </tbody> </table> </section> <script> function getFormatDate(date) { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}${month}${day}`; } async function displayMovieRanking(n) { const today = new Date(); const d = today.getTime() - (n * 24 * 60 * 60 * 1000); const targetday = new Date(d); const targetDt = getFormatDate(targetday); const kobisUrl = `http://www.kobis.or.kr/kobisopenapi/webservice/rest/boxoffice/searchDailyBoxOfficeList.json?key=발급받은_영화진흥위원회_API_KEY&targetDt=${targetDt}`; const dom = document.getElementsByTagName('tbody')[0]; dom.innerHTML = ""; try { const response = await fetch(kobisUrl); const jsonObj = await response.json(); const dailyBoxOfficeList = jsonObj.boxOfficeResult.dailyBoxOfficeList; for (let i = 0; i < 10; i++) { const movie = dailyBoxOfficeList[i]; const movieNm = movie.movieNm; const imgResponse = await fetch(`https://dapi.kakao.com/v2/search/image?query=${encodeURIComponent(movieNm)}&sort=accuracy&page=1&size=1`, { headers: { "Authorization": "KakaoAK 발급받은_카카오_REST_API_KEY" } }); const imgJson = await imgResponse.json(); const thumbnailUrl = imgJson.documents.length > 0 ? imgJson.documents[0].thumbnail_url : 'https://via.placeholder.com/50'; const content = ` <tr> <td scope='row'><img src='${thumbnailUrl}' width='50'></td> <td>${movieNm}</td> <td>${Number(movie.audiCnt).toLocaleString()}</td> <td>${Number(movie.audiAcc).toLocaleString()}</td> <td>${movie.scrnCnt}</td> </tr>`; dom.innerHTML += content; } } catch (error) { console.error("데이터를 불러오는 중 오류가 발생했습니다:", error); } } </script> </body> </html>이 코드는 크게 세 파일로 나누어 볼 수 있다.
첫 번째exam15.html은 박스오피스 목록을 가져온 뒤, 각 영화명으로 이미지 검색 요청을 다시 보내는 기본 중첩fetch()방식이다.
두 번째exam15_1.html은 여러 이미지 검색 요청을Promise.all()로 모아서 처리하는 방식이다.
세 번째exam15_2.html은 같은 흐름을async/await로 작성한 방식이다.
세 파일 모두 화면 구조는 같다.
상단에는1일전,2일전,3일전버튼이 있고, 아래에는 영화 썸네일, 영화명, 해당일 관객수, 누적 관객수, 스크린 수를 출력할 테이블이 있다.
세 예제는 같은 종류의 데이터를 출력하지만, 여러 비동기 요청을 처리하는 방식과 화면 갱신 순서가 다를 수 있다.
이 예제에서 확인할 핵심
- 영화진흥위원회 박스오피스
API로 일별 영화 순위 데이터를 가져온다.- 카카오 이미지 검색
API로 각 영화명의 썸네일 이미지를 가져온다.1일전,2일전,3일전버튼은 조회할 날짜를 다르게 만든다.getFormatDate()는 날짜를yyyyMMdd형식으로 만든다.- 박스오피스 응답에서는 영화명, 해당일 관객수, 누적 관객수, 스크린 수를 꺼낸다.
- 영화명은 카카오 이미지 검색 요청의 검색어로 다시 사용된다.
exam15.html은 영화 목록 요청 뒤 이미지 요청을 반복해서 보내는 중첩fetch()방식이다.exam15.html은 이미지 요청이 끝나는 순서에 따라 테이블에 추가되는 순서가 달라질 수 있다.exam15_1.html은 여러 이미지 요청을Promise.all()로 모아 기다린 뒤 한 번에 출력한다.exam15_2.html은async/await로 박스오피스 요청과 이미지 요청을 순서대로 읽기 쉽게 작성한다.이 예제는 외부
Open API요청 결과를 다시 다른Open API요청의 입력값으로 사용해서 화면을 구성하는 예제이다.
Open API란 무엇인가
Open API는 외부에서 정해진 방식으로 사용할 수 있도록 공개된API이다.
API는 프로그램끼리 데이터를 주고받기 위한 약속이라고 이해하면 된다.
이 예제에서는 두 개의 외부Open API를 사용한다.
첫 번째는 영화진흥위원회 박스오피스API이다.
이API는 날짜를 기준으로 일별 박스오피스 순위 데이터를 제공한다.
두 번째는 카카오 이미지 검색API이다.
이API는 검색어를 기준으로 이미지 검색 결과를 제공한다.
여기서는 영화명을 검색어로 사용해서 영화 썸네일 이미지를 가져온다.
이 예제는 하나의 외부API결과를 다른 외부API요청에 다시 활용하는 흐름을 보여 준다.
버튼은 조회 날짜를 정한다
// exam15.html <button type="button" class="btn btn-outline-primary" onclick="displayMovieRanking(1)">1일전</button> <button type="button" class="btn btn-outline-info" onclick="displayMovieRanking(2)">2일전</button> <button type="button" class="btn btn-outline-success" onclick="displayMovieRanking(3)">3일전</button>세 버튼은 모두
displayMovieRanking()함수를 호출한다.
차이는 전달하는 숫자이다.
1일전버튼은displayMovieRanking(1)을 실행한다.
2일전버튼은displayMovieRanking(2)를 실행한다.
3일전버튼은displayMovieRanking(3)을 실행한다.
여기서 전달하는 숫자n은 오늘 날짜에서 며칠 전 데이터를 조회할지 정하는 값이다.
즉,n이1이면 하루 전 날짜를 기준으로 박스오피스를 조회한다.
버튼에서 전달하는 숫자는 박스오피스 조회 날짜를 정하는 기준값이다.
getFormatDate는 날짜를 yyyyMMdd 형식으로 만든다
// exam15.html function getFormatDate(date) { const year = date.getFullYear(); let month = (1 + date.getMonth()); month = month >= 10 ? month : '0' + month; let day = date.getDate(); day = day >= 10 ? day : '0' + day; return "" + year + month + day; }영화진흥위원회 박스오피스
API는 조회 날짜를yyyyMMdd형식으로 받는다.
예를 들어2026년5월15일이면20260515처럼 만들어야 한다.
getFormatDate()함수는Date객체를 받아서yyyyMMdd문자열로 바꾼다.
getFullYear()는 연도를 가져온다.
getMonth()는 월을 가져오지만,0부터 시작하기 때문에1을 더한다.
getDate()는 일을 가져온다.
월과 일이 한 자리 숫자이면 앞에0을 붙인다.
그래야5월이5가 아니라05가 되고,7일이7이 아니라07이 된다.
외부API가 요구하는 날짜 형식에 맞춰 데이터를 보내야 정상 응답을 받을 수 있다.
displayMovieRanking은 버튼 클릭 후 전체 흐름을 실행한다
// exam15.html function displayMovieRanking(n) { const today = new Date(); const d = today.getTime() - (n * 24 * 60 * 60 * 1000); let targetday = new Date(d); const targetDt = getFormatDate(targetday); let url = 'http://www.kobis.or.kr/kobisopenapi/webservice/rest/boxoffice/searchDailyBoxOfficeList.json?key=발급받은_영화진흥위원회_API_KEY&targetDt=' + targetDt; const dom = document.getElementsByTagName('tbody')[0]; }
displayMovieRanking(n)은 버튼을 클릭했을 때 실행되는 핵심 함수이다.
이 함수는 조회할 날짜를 계산하고, 박스오피스API요청 주소를 만든다.
today.getTime()은 현재 시간을 밀리초 단위로 가져온다.
여기서n * 24 * 60 * 60 * 1000을 빼면n일 전 시간이 된다.
계산된 날짜는getFormatDate()를 거쳐yyyyMMdd형식으로 바뀐다.
이 값이targetDt이다.
targetDt는 영화진흥위원회API요청 주소에 붙는다.
tbody는 영화 목록을 실제로 출력할 위치이다.
조회 결과가 만들어지면 이tbody안에tr태그를 추가한다.
displayMovieRanking()은 날짜 계산, 박스오피스 요청, 이미지 요청, 테이블 출력까지 이어지는 전체 흐름의 시작점이다.
exam15.html은 중첩 fetch 방식으로 처리한다
// exam15.html fetch(url) .then(response => { return response.json(); }) .then(jsonObj => { for (let i = 0; i < 10; i++) { movieNm[i] = jsonObj.boxOfficeResult.dailyBoxOfficeList[i].movieNm; audiCnt[i] = jsonObj.boxOfficeResult.dailyBoxOfficeList[i].audiCnt; audiAcc[i] = jsonObj.boxOfficeResult.dailyBoxOfficeList[i].audiAcc; scrnCnt[i] = jsonObj.boxOfficeResult.dailyBoxOfficeList[i].scrnCnt; } return movieNm; }) .then(mNm => { dom.innerHTML = ""; for (let i = 0; i < 10; i++) { fetch('https://dapi.kakao.com/v2/search/image?query=' + encodeURIComponent(mNm[i]) + '&sort=accuracy& page=1&size=5', { headers: { "Authorization" : "KakaoAK 발급받은_카카오_REST_API_KEY" } }) .then(response => { return response.json(); }) .then(jsonObj => { content = "<tr><td scope='row'><img src='" + jsonObj.documents[0].thumbnail_url + "' width='50'>" + "</td><td>" + movieNm[i] + "</td><td>" + audiCnt[i].toLocaleString() + "</td><td>" + audiAcc[i].toLocaleString() + "</td><td>" + scrnCnt[i] + "</td></tr>"; dom.innerHTML += content; }); } });
exam15.html은 먼저 박스오피스API를 요청한다.
응답을JSON으로 변환한 뒤, 상위10개의 영화 정보를 배열에 저장한다.
이후 영화명 배열을 이용해서 카카오 이미지 검색API요청을 반복한다.
즉, 박스오피스 요청이 한 번 있고, 그 결과로 이미지 검색 요청이 여러 번 만들어진다.
이 방식은 구조를 이해하기 쉽지만, 이미지 요청이 각각 따로 끝난다.
각 이미지 요청이 끝날 때마다dom.innerHTML += content로 테이블에 한 줄씩 추가한다.
그래서 이미지 응답이 도착하는 순서에 따라 화면에 추가되는 영화 행 순서가 달라질 수 있다.
중첩fetch()방식은 박스오피스 결과를 받은 뒤, 각 영화 이미지 요청을 따로 보내고 완료되는 대로 화면에 추가한다.
exam15_1.html은 Promise.all로 여러 이미지 요청을 모아 처리한다
// exam15_1.html const imagePromises = movieList.map((movie, index) => { return fetch('https://dapi.kakao.com/v2/search/image?query=' + encodeURIComponent(movie.movieNm) + '&sort=accuracy&size=1', { headers: { "Authorization": "KakaoAK 발급받은_카카오_REST_API_KEY" } }) .then(res => res.json()) .then(imgData => { const thumb = imgData.documents.length > 0 ? imgData.documents[0].thumbnail_url : 'https://via.placeholder.com/50'; return { name: movie.movieNm, audiCnt: movie.audiCnt, audiAcc: movie.audiAcc, scrnCnt: movie.scrnCnt, img: thumb }; }); }); return Promise.all(imagePromises);
exam15_1.html은 이미지 검색 요청을Promise배열로 만든다.
map()은 영화 목록을 하나씩 돌면서 각 영화에 대한 이미지 검색fetch()를 만든다.
이렇게 만들어진 여러Promise가imagePromises배열에 들어간다.
그다음Promise.all(imagePromises)를 사용한다.
Promise.all()은 배열 안에 있는 모든Promise가 끝날 때까지 기다린다.
모든 이미지 요청이 끝나면, 각 영화 정보와 이미지 주소가 합쳐진 최종 데이터가 만들어진다.
그 후finalData를 이용해 테이블 내용을 한 번에 구성하고dom.innerHTML = content로 출력한다.
Promise.all()을 사용하면 여러 이미지 요청이 모두 끝난 뒤, 순서가 맞춰진 최종 데이터를 한 번에 화면에 출력할 수 있다.
exam15_2.html은 async await 방식으로 순서대로 읽기 쉽게 처리한다
// exam15_2.html async function displayMovieRanking(n) { const today = new Date(); const d = today.getTime() - (n * 24 * 60 * 60 * 1000); const targetday = new Date(d); const targetDt = getFormatDate(targetday); const kobisUrl = `http://www.kobis.or.kr/kobisopenapi/webservice/rest/boxoffice/searchDailyBoxOfficeList.json?key=발급받은_영화진흥위원회_API_KEY&targetDt=${targetDt}`; const dom = document.getElementsByTagName('tbody')[0]; dom.innerHTML = ""; try { const response = await fetch(kobisUrl); const jsonObj = await response.json(); const dailyBoxOfficeList = jsonObj.boxOfficeResult.dailyBoxOfficeList; for (let i = 0; i < 10; i++) { const movie = dailyBoxOfficeList[i]; const movieNm = movie.movieNm; const imgResponse = await fetch(`https://dapi.kakao.com/v2/search/image?query=${encodeURIComponent(movieNm)}&sort=accuracy&page=1&size=1`, { headers: { "Authorization": "KakaoAK 발급받은_카카오_REST_API_KEY" } }); const imgJson = await imgResponse.json(); const thumbnailUrl = imgJson.documents.length > 0 ? imgJson.documents[0].thumbnail_url : 'https://via.placeholder.com/50'; const content = ` <tr> <td scope='row'><img src='${thumbnailUrl}' width='50'></td> <td>${movieNm}</td> <td>${Number(movie.audiCnt).toLocaleString()}</td> <td>${Number(movie.audiAcc).toLocaleString()}</td> <td>${movie.scrnCnt}</td> </tr>`; dom.innerHTML += content; } } catch (error) { console.error("데이터를 불러오는 중 오류가 발생했습니다:", error); } }
exam15_2.html은 같은 기능을async/await방식으로 작성한다.
await fetch(kobisUrl)로 박스오피스 응답을 기다린다.
그다음await response.json()으로 응답을JSON데이터로 변환한다.
이후for반복문 안에서 영화 하나씩 처리한다.
각 영화명으로 카카오 이미지 검색 요청을 보내고,await imgResponse.json()으로 이미지 검색 결과를 기다린다.
이 방식은 코드 흐름이 위에서 아래로 읽히기 쉽다.
다만 반복문 안에서await를 사용하기 때문에, 이미지 요청을 하나씩 기다리며 처리하는 구조가 된다.
async/await방식은 박스오피스 요청과 이미지 검색 요청의 순서를 읽기 쉽게 보여 주지만, 반복문 안에서 하나씩 기다리는 구조라는 점도 함께 봐야 한다.
세 방식의 차이 비교하기
세 파일은 모두 박스오피스 정보와 영화 썸네일을 테이블에 출력한다.
버튼을 클릭하면 박스오피스 데이터를 가져오고, 영화명으로 이미지를 검색해서 테이블을 구성한다.
다만 여러 비동기 요청을 처리하는 방식이 다르다.
exam15.html은 중첩fetch()방식이다.
이 방식은 각 이미지 요청이 끝나는 대로 테이블 행을 추가하므로, 이미지 응답 순서에 따라 화면에 추가되는 순서가 달라질 수 있다.
exam15_1.html은Promise.all()방식이다.
여러 이미지 요청을 모두 기다린 뒤 최종 데이터를 구성하므로, 영화 정보와 이미지 주소를 맞춰 한 번에 출력할 수 있다.
exam15_2.html은async/await방식이다.
박스오피스 요청을 기다린 뒤, 반복문 안에서 영화별 이미지 요청을 순서대로 기다리며 테이블 행을 추가한다.
정리하면 아래처럼 볼 수 있다.
exam15.html은 이미지 요청이 끝나는 대로 테이블 행을 추가한다.exam15.html은 응답 순서에 따라 화면에 추가되는 순서가 달라질 수 있다.exam15_1.html은 이미지 요청들을Promise.all()로 모두 기다린 뒤 한 번에 출력한다.exam15_2.html은await를 사용해 요청 흐름을 위에서 아래로 읽기 쉽게 작성한다.- 세 방식 모두 박스오피스
API와 카카오 이미지 검색API를 함께 사용한다.- 세 방식 모두 최종적으로 영화 썸네일, 영화명, 해당일 관객수, 누적 관객수, 스크린 수를 출력한다.
같은 데이터를 출력하더라도 여러 비동기 요청을 어떻게 기다리고 출력할지에 따라 코드 구조와 화면 갱신 순서가 달라질 수 있다.
실제 출력 결과 확인하기
이 예제를 실행하면 화면 상단에
일별 박스 오피스제목과1일전,2일전,3일전버튼이 보인다.
버튼을 클릭하면 해당 날짜의 박스오피스 목록이 테이블에 출력된다.
테이블에는 영화 썸네일, 영화명, 해당일 관객수, 누적 관객수, 스크린 수가 표시된다.
날짜 버튼을 바꾸면targetDt가 달라지고, 그에 따라 다른 날짜의 박스오피스 결과가 다시 출력된다.
exam15.html에서는 이미지 요청이 각각 끝나는 대로 테이블 행이 채워지는 흐름을 볼 수 있다.
따라서 이미지 응답 순서에 따라 화면에 추가되는 순서가 달라질 수 있다.
exam15_1.html에서는 여러 이미지 요청을 모아 처리한 뒤 테이블 결과가 정리되어 출력되는 흐름을 볼 수 있다.
exam15_2.html에서는async/await흐름으로 영화별 이미지 요청을 기다리며 테이블 행을 추가하는 흐름을 볼 수 있다.
이 예제의 결과에서 중요한 부분은 버튼을 클릭할 때마다 외부API요청이 실행되고, 영화 목록과 썸네일이 함께 테이블에 출력된다는 점이다.
결과물 뽑기
이 예제의 결과물은 브라우저 화면을 중심으로 확인한다.
버튼을 클릭했을 때 박스오피스 테이블에 영화 썸네일, 영화명, 해당일 관객수, 누적 관객수, 스크린 수가 출력되는지 확인하면 된다.
먼저exam15.html을 브라우저에서 실행한다.
1일전,2일전,3일전버튼을 클릭하면 박스오피스 데이터가 요청되고, 각 영화명으로 이미지 검색 요청이 다시 실행된다.// exam15.html 출력결과 // 버튼 클릭 // 박스오피스 API 요청 // 영화명별 카카오 이미지 검색 API 요청 // 이미지 응답이 끝나는 순서에 따라 테이블 행이 추가됨
exam15.html은 중첩fetch()방식이므로, 박스오피스 목록을 받은 뒤 각 영화 이미지 요청이 완료되는 순서에 따라 테이블 행이 채워질 수 있다.
다음으로exam15_1.html을 브라우저에서 실행한다.
같은 버튼을 클릭하면 여러 이미지 검색 요청을Promise.all()로 모아 기다린 뒤, 최종 데이터를 테이블에 출력한다.// exam15_1.html 출력결과 // 버튼 클릭 // 박스오피스 API 요청 // 각 영화 이미지 검색 요청들을 Promise.all()로 대기 // 영화 썸네일과 박스오피스 정보가 테이블에 출력됨
exam15_1.html은 여러 이미지 요청을 모두 기다린 뒤 최종 데이터를 구성한다.
그래서 영화 정보와 이미지 주소를 합친 데이터를 기준으로 테이블을 출력한다.
마지막으로exam15_2.html을 브라우저에서 실행한다.
1일전,2일전,3일전버튼을 클릭하면async/await방식으로 박스오피스 데이터와 영화 이미지 데이터를 요청한다.
await fetch()로 박스오피스 응답을 기다리고, 반복문 안에서 영화별 이미지 검색 결과도 기다린 뒤 테이블에 행을 추가한다.// exam15_2.html 출력결과 // 버튼 클릭 // 박스오피스 API 요청 // async/await로 영화별 이미지 검색 요청 처리 // 영화 썸네일과 박스오피스 정보가 테이블에 출력됨
exam15_2.html은async/await방식으로 박스오피스 요청과 이미지 검색 요청을 처리한다.
화면 결과는 영화 썸네일, 영화명, 해당일 관객수, 누적 관객수, 스크린 수가 테이블에 출력되는 형태이다.
이 결과에서 중요한 부분은 세 가지이다.
첫 번째는 버튼 클릭으로 조회 날짜가 바뀐다는 점이다.
두 번째는 박스오피스API결과에서 영화명을 꺼내 다시 카카오 이미지 검색API요청에 사용한다는 점이다.
세 번째는 여러 비동기 요청을 처리하는 방식에 따라 코드 구조와 화면 갱신 순서가 달라진다는 점이다.
이 예제의 최종 결과물은 외부Open API두 개를 함께 사용해 영화 순위와 영화 썸네일을 테이블에 출력하는 화면이다.
핵심 정리
이 예제의 핵심은 외부
Open API를 여러 번 호출해서 하나의 화면을 구성하는 흐름이다.
박스오피스API로 영화 순위 데이터를 가져오고, 각 영화명으로 카카오 이미지 검색API를 다시 호출한다.
이 흐름은 아래처럼 정리할 수 있다.
1일전,2일전,3일전버튼은 조회 날짜를 정한다.getFormatDate()는 날짜를yyyyMMdd형식으로 만든다.- 영화진흥위원회 박스오피스
API에targetDt를 붙여 요청한다.- 응답에서 상위
10개 영화 정보를 꺼낸다.- 영화명, 해당일 관객수, 누적 관객수, 스크린 수를 사용한다.
- 영화명은 카카오 이미지 검색
API의 검색어로 다시 사용한다.- 카카오 이미지 검색 응답에서 썸네일 주소를 꺼낸다.
- 테이블에는 영화 썸네일, 영화명, 해당일 관객수, 누적 관객수, 스크린 수가 출력된다.
exam15.html은 중첩fetch()방식이다.exam15.html은 이미지 응답 순서에 따라 화면 추가 순서가 달라질 수 있다.exam15_1.html은Promise.all()방식이다.exam15_2.html은async/await방식이다.- 같은 데이터를 출력하더라도 여러 비동기 요청을 처리하는 방식에 따라 코드 구조가 달라진다.
따라서 이 예제가 알려 주는 핵심은 이것이다.
외부Open API를 함께 사용할 때는 첫 번째 요청 결과를 다음 요청의 입력값으로 사용할 수 있으며, 여러 비동기 요청을 어떻게 기다리고 출력할지에 따라 중첩fetch(),Promise.all(),async/await방식으로 코드를 구성할 수 있다.
이번 추가 예제 흐름은setTimeout()비동기 처리에서 시작해Promise,async/await,fetch(),AJAX,REST API,Blob, 외부Open API까지 이어지는 비동기 처리 흐름으로 정리할 수 있다.