JavaScript 1주차 공부 내용 복습 및 정리

신동연·2021년 7월 12일
0

1. 화살표 함수(arrow function)

화살표 함수라는 이름은 문법의 생김새를 차용해 지어졌다.
화살표 함수는 함수 표현식보다 단순하고 간결한 문법으로 함수를 만들 수 있다는 장점이 있다.

let sum = (a, b) => a + b;
/* 위 화살표 함수는 아래 함수의 축약 버전입니다.
let sum = function(a, b) {
  return a + b;
};
*/
alert( sum(1, 2) ); // 3

위와 같이 (a, b) => a + b는 인수 a와 b를 받는 함수이다. (a, b) => a + b는 실행되는 순간 표현식 a + b를 평가하고 그 결과를 반환한다.

  • 인수가 하나밖에 없다면 인수를 감싸는 괄호를 생략할 수 있다.
    괄호를 생략하면 코드 길이를 더 줄일 수 있다.
let double = n => n * 2;
// let double = function(n) { return n * 2 }과 거의 동일합니다.
alert( double(3) ); // 6
  • 인수가 하나도 없을 땐 괄호를 비워놓으면 된다. 다만, 괄호는 생략할 수 없다.
let sayHi = () => alert("안녕하세요!");
sayHi();

본문이 여러 줄인 화살표 함수
평가해야 할 표현식이나 구문이 여러 개인 함수가 있을 수도 있는데 이때는 중괄호 안에 평가해야 할 코드를 넣어주어야 한다. 그리고 return 지시자를 사용해 명시적으로 결괏값을 반환해 주어야 한다.

let sum = (a, b) => {  // 중괄호는 본문 여러 줄로 구성되어 있음을 알려줍니다.
  let result = a + b;
  return result; // 중괄호를 사용했다면, return 지시자로 결괏값을 반환해주어야 합니다.
};
alert( sum(1, 2) ); // 3

2. 프라미스(Promise)

프라미스는 자바스크립트 비동기 처리에 사용되는 객체이다. 비동기 처리란 '특정 코드의 실행이 완료될 때까지 기다리지 않고 다음 코드를 먼저 수행하는 자바스크립트의 특성'을 의미한다.
그리고 프라미스는 주로 서버에서 받아온 데이터를 화면에 표시할 때 사용된다.

promise 객체는 아래와 같은 문법으로 만들 수 있다.

let promise = new Promise(function(resolve, reject) {
  // executor (제작 코드, '가수')
});

new Promise에 전달되는 함수는 executor(실행자, 실행 함수) 라고 부른다. executor는 new Promise가 만들어질 때 자동으로 실행되는데, 결과를 최종적으로 만들어내는 제작 코드를 포함한다.

executor의 인수 resolve와 reject는 자바스크립트에서 자체 제공하는 콜백이다.

  • resolve(value) - 일이 성공적으로 끝난 경우 그 결과를 나타내는 value와 함께 호출
  • reject(error) - 에러 발생 시 에러 객체를 나타내는 error와 함께 호출

new Promise 생성자가 반환하는 promise 객체는 아래와 같은 내부 프로퍼티를 갖는다.

  • state - 처음에는 "pending"(보류)였다가 resolve가 호출되면 "fulfillled"로, reject가 호출되면 "rejected"로 변한다.
  • result - 처음에는 "undefined"였다가 resolve가 호출되면 "value"로, reject가 호출되면 "error"로 변한다.

executor는 new Promise에 의해 자동으로 그리고 즉각적으로 호출된다.
executor는 인자로 resolve와 reject 함수를 받는다. 이 함수들은 자바스크립트 엔진이 미리 정의한 함수이므로 개발자가 따로 만들 필요는 없다.
다만, resolve나 reject 중 하나는 반드시 호출해야 한다.

let promise = new Promise(function(resolve, reject) {
  // 프라미스가 만들어지면 executor 함수는 자동으로 실행됩니다.
  
  // 1초 뒤에 일이 성공적으로 끝났다는 신호가 전달되면서 result는 'done'이 됩니다.
  setTimeout(() => resolve("done"), 1000);
});

위처럼 일이 성공적으로 처리되었을 때의 프라미스는 'fulfilled promise(약속이 이행된 프라미스)'라고 불린다.

아래는 executor가 에러와 함께 약속한 작업을 거부하는 경우이다.

let promise = new Promise(function(resolve, reject) {
  // 1초 뒤에 에러와 함께 실행이 종료되었다는 신호를 보냅니다.
  setTimeout(() => reject(new Error("에러 발생!")), 1000);
});

프라미스 객체는 executor와 결과나 에러를 받을 소비 함수를 이어주는 역할을 하는데 소비함수는 .then, .catch, .finally 메서드를 사용해 등록된다.

then

.then은 프라미스에서 가장 중요하고 기본이 되는 메서드이다.

promise.then(
  function(result) { /* 결과(result)를 다룹니다 */ },
  function(error) { /* 에러(error)를 다룹니다 */ }
);

.then의 첫 번째 인수는 프라미스가 이행되었을 때 실행되는 함수이고, 여기서 실행 결과를 받고 .then의 두 번째 인수는 프라미스가 거부되었을 때 실행되는 함수이며, 에러를 받는다.

작업이 성공적으로 처리된 경우만 다루고 싶다면 .then에 인수를 하나만 전달하면 된다.

catch

에러가 발생한 경우만 다루고 싶을때 .then(null, errorHandlingFunction)같이 null을 첫 번째 인수로 전달하면 된다. 그리고 .catch(errorHandlingFunction)를 써도 되는데, .catch는 .then에 null을 전달하는 것과 동일하게 작동한다.

let promise = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error("에러 발생!")), 1000);
});

// .catch(f)는 promise.then(null, f)과 동일하게 작동합니다
promise.catch(alert); // 1초 뒤 "Error: 에러 발생!" 출력

.catch(f)는 문법이 간결하다는 점만 빼고 .then(null,f)과 완벽하게 같다.

finally
try {...} catch {...}에 finally 절이 있는 것처럼, 프라미스에도 finally가 있다.
프라미스가 처리되면(이행이나 거부) f가 항상 실행된다는 점에서 .finally(f) 호출은 .then(f, f)과 유사하다.

쓸모가 없어진 로딩 인디케이터(loading indicator)를 멈추는 경우같이, 결과가 어떻든 마무리가 필요하면 finally가 유용하다.

new Promise((resolve, reject) => {
  /* 시간이 걸리는 어떤 일을 수행하고, 그 후 resolve, reject를 호출함 */
})
  // 성공·실패 여부와 상관없이 프라미스가 처리되면 실행됨
  .finally(() => 로딩 인디케이터 중지)
  .then(result => result와 err 보여줌 => error 보여줌)

finally는 .then(f, f)과 완전히 같지 않고 차이점이 있다.

  • finally 핸들러엔 인수가 없다. finally에선 프라미스가 이행되었는지, 거부되었는지 알 수 없으며 finally에선 절차를 마무리하는 ‘보편적’ 동작을 수행하기 때문에 성공·실패 여부를 몰라도 된다.

  • finally 핸들러는 자동으로 다음 핸들러에 결과와 에러를 전달한다.

프라미스 체이닝
프라미스 체이닝은 result가 .then 핸들러의 체인(사슬)을 통해 전달된다는 점에서 착안한 아이디어이다.

아래 예시는 아래와 같은 순서로 실행된다.

new Promise(function(resolve, reject) {

  setTimeout(() => resolve(1), 1000); // (*)

}).then(function(result) { // (**)

  alert(result); // 1
  return result * 2;

}).then(function(result) { // (***)

  alert(result); // 2
  return result * 2;

}).then(function(result) {

  alert(result); // 4
  return result * 2;

});
  • 1초 후 최초 프라미스가 이행된다. – (*)
  • 이후 첫번째 .then 핸들러가 호출된다. –(**)
  • 2에서 반환한 값은 다음 .then 핸들러에 전달된다. – (***)
  • 이런 과정이 계속 이어진다.

프라미스 체이닝이 가능한 이유는 promise.then을 호출하면 프라미스가 반환되기 때문이다. 반환된 프라미스엔 당연히 .then을 호출할 수 있다.

프라미스 반환하기
.then(handler)에 사용된 핸들러가 프라미스를 생성하거나 반환하는 경우도 있다. 이 경우 이어지는 핸들러는 프라미스가 처리될 때까지 기다리다가 처리가 완료되면 그 결과를 받는다.

new Promise(function(resolve, reject) {

  setTimeout(() => resolve(1), 1000);

}).then(function(result) {

  alert(result); // 1

  return new Promise((resolve, reject) => { // (*)
    setTimeout(() => resolve(result * 2), 1000);
  });

}).then(function(result) { // (**)

  alert(result); // 2

  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(result * 2), 1000);
  });

}).then(function(result) {

  alert(result); // 4

});

예시에서 첫 번째 .then은 1을 출력하고 new Promise(…)를 반환한다.
1초 후 이 프라미스가 이행되고 그 결과(resolve의 인수인 result * 2)는 두 번째 .then으로 전달된다. 두 번째 핸들러((**))는 2를 출력하고 동일한 과정이 반복된다.

따라서 얼럿 창엔 이전 예시와 동일하게 1, 2, 4가 차례대로 출력된다. 다만 얼럿 창 사이에 1초의 딜레이가 생긴다.

이렇게 핸들러 안에서 프라미스를 반환하는 것도 비동기 작업 체이닝을 가능하게 해준다.

프라미스와 에러 핸들링

프라미스가 거부되면 제어 흐름이 제일 가까운 rejection 핸들러로 넘어가기 때문에 프라미스 체인을 사용하면 에러를 쉽게 처리할 수 있다. 이는 실무에서 아주 유용한 기능이다.

fetch('https://no-such-server.blabla') // 거부
  .then(response => response.json())
  .catch(err => alert(err)) // TypeError: failed to fetch (출력되는 내용은 다를 수 있음)

예시에서 보듯 .catch는 첫번째 핸들러일 필요가 없고 하나 혹은 여러 개의 .then 뒤에 올 수 있다.

이번에는 사이트에는 아무런 문제가 없지만 응답으로 받은 JSON의 형식이 잘못된 경우를 살펴보면 가장 쉬운 에러 처리 방법은 체인 끝에 .catch를 붙이는 것이다.

fetch('/article/promise-chaining/user.json')
  .then(response => response.json())
  .then(user => fetch(`https://api.github.com/users/${user.name}`))
  .then(response => response.json())
  .then(githubUser => new Promise((resolve, reject) => {
    let img = document.createElement('img');
    img.src = githubUser.avatar_url;
    img.className = "promise-avatar-example";
    document.body.append(img);

    setTimeout(() => {
      img.remove();
      resolve(githubUser);
    }, 3000);
  }))
  .catch(error => alert(error.message));

정상적인 경우라면 .catch는 절대 트리거 되지 않는다. 그런데 네트워크 문제, 잘못된 형식의 JSON 등으로 인해 위쪽 프라미스 중 하나라도 거부되면 .catch에서 에러를 잡게 된다.

프라미스 executor와 프라미스 핸들러 코드 주위엔 '보이지 않는(암시적) try..catch'가 있는데 예외가 발생하면 암시적 try..catch에서 예외를 잡고 이를 reject처럼 다룬다.

new Promise((resolve, reject) => {
  throw new Error("에러 발생!");
}).catch(alert); // Error: 에러 발생!
new Promise((resolve, reject) => {
  reject(new Error("에러 발생!"));
}).catch(alert); // Error: 에러 발생!

체인 마지막의 .catch는 try..catch와 유사한 역할을 한다. .then 핸들러를 원하는 만큼 사용하다 마지막에 .catch 하나만 붙이면 .then 핸들러에서 발생한 모든 에러를 처리할 수 있다.

일반 try..catch에선 에러를 분석하고, 처리할 수 없는 에러라 판단되면 에러를 다시 던질 때가 있는데 프라미스에도 유사한 일을 할 수 있다.

.catch 안에서 throw를 사용하면 제어 흐름이 가장 가까운 곳에 있는 에러 핸들러로 넘어가는데 여기서 에러가 성공적으로 처리되면 가장 가까운 곳에 있는 .then 핸들러로 제어 흐름이 넘어가 실행이 이어진다.

// 실행 순서: catch -> then
new Promise((resolve, reject) => {

  throw new Error("에러 발생!");

}).catch(function(error) {

  alert("에러가 잘 처리되었습니다. 정상적으로 실행이 이어집니다.");

}).then(() => alert("다음 핸들러가 실행됩니다."));

추가적으로 unhandledrejection 이벤트 핸들러를 사용하면 처리되지 않은 에러를 추적하고, 이를 사용자(혹은 서버에)에게 알려서 애플리케이션이 아무런 설명도 없이 ‘그냥 죽는걸’ 방지할 수 있다. 브라우저 환경에선 예방에 unhandledrejection을, 다른 환경에선 유사한 핸들러를 사용할 수 있다.

프라미스 API
Promise 클래스에는 5가지 정적 메서드가 있다.

  • Promise.all(promises) – 모든 프라미스가 이행될 때까지 기다렸다가 그 결괏값을 담은 배열을 반환한다. 주어진 프라미스 중 하나라도 실패하면 Promise.all는 거부되고, 나머지 프라미스의 결과는 무시된다.

  • Promise.allSettled(promises) – 최근에 추가된 메서드로 모든 프라미스가 처리될 때까지 기다렸다가 그 결과(객체)를 담은 배열을 반환한다. 객체엔 다음과 같은 정보가 담긴다.
    status: "fulfilled" 또는 "rejected"
    value(프라미스가 성공한 경우) 또는 reason(프라미스가 실패한 경우)

  • Promise.race(promises) – 가장 먼저 처리된 프라미스의 결과 또는 에러를 담은 프라미스를 반환한다.

  • Promise.resolve(value) – 주어진 값을 사용해 이행 상태의 프라미스를 만든다.

  • Promise.reject(error) – 주어진 에러를 사용해 거부 상태의 프라미스를 만든다.

프라미스화
콜백을 받는 함수를 프라미스를 반환하는 함수로 바꾸는 것을 '프라미스화(promisification)'라고 한다.

function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;

  script.onload = () => callback(null, script);
  script.onerror = () => callback(new Error(`${src}를 불러오는 도중에 에러가 발생함`));

  document.head.append(script);
}

// usage:
// loadScript('path/script.js', (err, script) => {...})

loadScript(src, callback)를 이제 프라미스화해보면 새로운 함수 loadScriptPromise(src)는 loadScript와 동일하게 동작하지만 callback을 제외한 src만 인수로 받아야 하고, 프라미스를 반환해야 한다.

let loadScriptPromise = function(src) {
  return new Promise((resolve, reject) => {
    loadScript(src, (err, script) => {
      if (err) reject(err)
      else resolve(script);
    });
  })
}

// 사용법:
// loadScriptPromise('path/script.js').then(...)

loadScriptPromise는 기존 함수 loadScript에 모든 일을 위임한다. loadScript의 콜백은 스크립트 로딩 상태에 따라 이행 혹은 거부상태의 프라미스를 반환한다.

3. async

async 키워드부터 알아보면 먼저 async는 function 앞에 위치한다.

async function f() {
  return 1;
}

function 앞에 async를 붙이면 해당 함수는 항상 프라미스를 반환한다. 프라미스가 아닌 값을 반환하더라도 이행 상태의 프라미스(resolved promise)로 값을 감싸 이행된 프라미스가 반환되도록 한다.

아래 예시의 함수를 호출하면 result가 1인 이행 프라미스가 반환된다.

async function f() {
  return 1;
}

f().then(alert); // 1

명시적으로 프라미스를 반환하는 것도 가능한데, 결과는 동일하다.

async function f() {
  return Promise.resolve(1);
}

f().then(alert); // 1

async가 붙은 함수는 반드시 프라미스를 반환하고, 프라미스가 아닌 것은 프라미스로 감싸 반환한다. 그런데 async가 제공하는 기능은 이뿐만이 아니다.

또 다른 키워드 await는 async 함수 안에서 동작한다.

4. await

// await는 async 함수 안에서만 동작합니다.
let value = await promise;

자바스크립트는 await 키워드를 만나면 프라미스가 처리(settled)될 때까지 기다리는데 결과는 그 이후 반환된다.

async function f() {

  let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve("완료!"), 1000)
  });

  let result = await promise; // 프라미스가 이행될 때까지 기다림 (*)

  alert(result); // "완료!"
}

f();

함수를 호출하고, 함수 본문이 실행되는 도중에 (*)로 표시한 줄에서 실행이 잠시 '중단’되었다가 프라미스가 처리되면 실행이 재개된다. 이때 프라미스 객체의 result 값이 변수 result에 할당된다. 따라서 위 예시를 실행하면 1초 뒤에 '완료!'가 출력된다.

await는 말 그대로 프라미스가 처리될 때까지 함수 실행을 기다리게 만든다. 프라미스가 처리되면 그 결과와 함께 실행이 재개되고 프라미스가 처리되길 기다리는 동안엔 엔진이 다른 일(다른 스크립트를 실행, 이벤트 처리 등)을 할 수 있기 때문에, CPU 리소스가 낭비되지 않는다.

await는 promise.then보다 좀 더 세련되게 프라미스의 result 값을 얻을 수 있도록 해주는 문법이다. promise.then보다 가독성 좋고 쓰기도 쉽다.

+에러 핸들링

프라미스가 정상적으로 이행되면 await promise는 프라미스 객체의 result에 저장된 값을 반환한다. 반면 프라미스가 거부되면 마치 throw문을 작성한 것처럼 에러가 던져진다.

async function f() {
  await Promise.reject(new Error("에러 발생!"));
}
async function f() {
  throw new Error("에러 발생!");
}

실제 상황에선 프라미스가 거부 되기 전에 약간의 시간이 지체되는 경우가 있는데 이런 경우엔 await가 에러를 던지기 전에 지연이 발생한다.

await가 던진 에러는 throw가 던진 에러를 잡을 때처럼 try..catch를 사용해 잡을 수 있다.

async function f() {

  try {
    let response = await fetch('http://유효하지-않은-주소');
  } catch(err) {
    alert(err); // TypeError: failed to fetch
  }
}

f();

try..catch가 없으면 아래 예시의 async 함수 f()를 호출해 만든 프라미스가 거부 상태가 된다. f()에 .catch를 추가하면 거부된 프라미스를 처리할 수 있다.

async function f() {
  let response = await fetch('http://유효하지-않은-url');
}

// f()는 거부 상태의 프라미스가 됩니다.
f().catch(alert); // TypeError: failed to fetch // (*)

.catch를 추가하는 걸 잊으면, 처리되지 않은 프라미스 에러가 발생한다. 이런 에러는 전역 이벤트 핸들러 unhandledrejection을 사용해 잡을 수 있다.

위 내용은 JAVASCRIPT.INFO 라는 사이트를 참고하여 공부하였습니다.

0개의 댓글