ES6 Module 

모던 브라우저(2019.11~)에서는 흔히 말하는 웹팩, 즉 번들러없이 ES6 모듈을 쓸 수 있다.

 

import

export 키워드로 내보내진 변수, 함수 등을 불러올 수 있는 키워드이다. 

// module-name 내에 export default로 내보내진 것을 가져온다.
import defaultExport from "module-name";

// module-name 내에서 export된 모든 것을 모두 가져온다.
// as 이후 이름은 중복되지 않으면 자유롭게 정할 수 있다.
import * as allItems from "module-name";

// module-name 내에서 export된 것 중에 특정 값만 가져온다.
import { loadItem } from "module-name";

// module-name 내에서 export된 것 중에 특정 값만 이름을 바꿔서 가져온다.
import { loadItem as loadSomething } from "module-name";

// export default된 것과 개별 export된 것을 한번에 가져올 수도 있다.
import defaultExport, { loadItem } from "module-name";

// 별도의 모듈 바인딩 없이 불러오기만 한다. 
// 불러오는 것만으로 효과가 있는 스크립트의 경우 사용된다.
import "module-name";

번들러는 필요한 모듈을 하나로 합쳐주기 때문에 쓰지 않을 모듈을 import하는 것보다는 필요한 모듈만 불러오는 것이 좋다.

 

TodoList에 모듈 적용

이전 포스팅의 투두리스트에 모듈을 적용해보자.

 

- index.html의 main.js 로딩 부분에 type="module" 추가 후 다른 스크립트 제거

모듈을 쓰는 경우 가장 처음 부르는 script 태그에는 type="module" 속성을 추가해 사용한다. 

...
<body>
  <main class="app"></main>
  <script src="./src/main.js" type="module"></script>
</body>
...

 

- Header, TodoForm, TodoList 컴포넌에 export default 붙이기

컴포넌트의 경우 하나의 파일에 하나의 컴포넌트 규칙이 널리 쓰이므로 컴포넌트를 export default로 지정한다.

Header, TodoForm, TodoList는 외부에서 불러오는 의존성이 없기 때문에 다른 의존성을 처리할 필요는 없다.

export default function Header...

이제 다른쪽에서 import로 꺼내올 수 있게 되었다.

 

- storage.js 수정

이전 코드의 경우 전역 오염을 최소화시키고 storage라는 네임 스페이스로만 노출시키기 위해 IIFE 방식을 사용했는데 더 이상 그럴 필요가 없다. 

모듈은 전역 오염이 일어나지 않는다

const storage = window.localStorage;

export const setItem = (key, value) => { // export 
  try {
    storage.setItem(key, value);
  } catch (e) {
    console.log(e);
  }
};

export const getItem = (key, defaultValue) => { // export
  try {
    const storedValue = storage.getItem(key);

    if (storedValue) {
      return JSON.parse(storedValue);
    }
    return defaultValue;
  } catch (e) {
    console.log(e);
    return defaultValue;
  }
};

 

- App.js에서 필요한 것들 불러오기

이전 코드에서는 index.html에서 script src로 불러왔다는 전제하에 호출되었던 것들(ex. Header, TodoForm, TodoList, setItem)을 명시적으로 선언해서 불러온다.

import Header from "./Header.js";
import TodoForm from "./TodoForm.js";
import TodoList from "./TodoList.js";
import { setItem } from "./storage.js";

export default function App({ $target, initialState }) { // export default
  ...
  new TodoForm({
    $target,
    onSubmit: (text) => {
      ...
      setItem("todos", JSON.stringify(nextState)); // storage.setItem -> setItem
    },
  });
 ...

 

import 구문은 항상 코드의 상단에 있어야 한다.

 

 - main.js에 import 선언

import App from "./App.js";
import { getItem } from "./storage.js";

const initialState = getItem("todos", []); // stroage.getItem -> getItem
...

 

모듈의 장점

  • 스크립트 의존성을 훨씬 간편하게 관리할 수 있다. 
  • 각 JS 별로 사용되는 모듈을 명시적으로 import하기 때문에, 사용되거나 사용되지 않는 스크립트를 추적할 수 있다.
  • script 태그로 로딩하는 경우 불러오는 순서가 중요하지만, import로 불러오는 경우 순서가 무관하다.
  • script src로 불러오는 것과 다르게 전역 오염이 일어나지 않는다.

 

import를 사용하려면 웹 서버가 필요하다.

npx serve 커맨드를 이용하여 serve 모듈로 로컬 웹 서버를 띄워서 진행하고 있다면 사용 가능하다. 

+ 뭔가 잘 안된다면 from 이후 모듈 이름 맨 뒤에 .js를 빼먹지 않았는지 확인하자!

 

비동기 다루기 callback

비동기 처리란?

특정 코드의 연산이 끝날 때까지 코드의 실행을 멈추지 않고, 다음 코드를 먼저 실행하는 자바스크립트의 특성

 

예시 1) addEventListener 함수

두 번째 인자로 넘겨진 함수는 바로 실행되지 않고, 이벤트 리스너가 정의한 이벤트가 발생할 때 실행된다. 

 

예시 2) setTimeout과 setInteral

첫 번째 인자로 넘겨진 함수는 바로 실행되지 않고, setTimeout 혹은 setInterval의 시간만큼 지난 후에 실행된다. 

참고로 setTimeout의 시간을 0으로 넣거나 지정하지 않는다고 해도 바로 실행이 되지 않는다. 

 

예시 3) XMLHttpRequest(XHR)

데이터를 비동기로 요청하고, 요청 후의 동작을 비동기로 처리한다. 

function request(url, successCallback, failCallback) {
  const xhr = new XMLHttpRequest();
  xhr.addEventListener("load", (e) => {
    if (xhr.readyState === 4) {
      if (xhr.status === 200) {
        successCallback(JSON.parse(xhr.responseText));
      } else {
        failCallback(xhr.statusText);
      }
    }
  });
  xhr.addEventListenr("error", (e) => failCallback(xhr.statusText));
  
  xhr.open("GET", url);
  xhr.send();
}

 

만일 앞에서 정의한 requeset 함수를 이용하여 다음의 시나리오를 처리한다면 코드는 다음과 같을 것이다.

  1. https://kdt.roto.codes/todos를 조회한다.
  2.  조회가 성공했다면, todo의 목록 중 완료 된 todo의 id로 comments를 불러온다. https://kdt.roto.codes/commets?todo.id={todoId}
  3. 불러온 comment 데이터 중 content를 화면에 그린다. 
const API_ENDPOINT = "https://kdt.roto.codes";

request(`${API_ENDPOINT}/todos`, (todos) => {
  const completedTodo = todos.find((todo) => todo.isCompleted);

  if (completedTodo) {
    request(
      `${API_ENDPOINT}/commets?todo.id=${completedTodo.id}`,
      (comments) => {
        comments.forEach((comment) => console.log(comment.content));
      }
    );
  }
});

이런 경우, 일반적인 callback 패턴인 경우는 callback이 중첩된다. 

만약 순차적으로 처리해야 하는 비동기 작업이 더 많아지면 바로 callback hell, 콜백 지옥이 발생한다. 

 

XMLHttpRequest의 경우 비동기를 포기하고 동기 방식으로 동작하게 할 수도 있지만 동기 방식으로 사용하게 되면 요청 후 응답이 오기 전까지 브라우저가 동작하지 않는다. 따라서 이 방식으로 사용하면 안 된다. 

 

Promise

Promise는 비동기 작업을 제어하기 위해 나온 개념으로 콜백 지옥에서 어느정도 벗어날 수 있게 해준다.

Promise로 정의된 작업끼리는 연결(체이닝)할 수 있으며, 이를 통해 코드의 depth가 크게 증가하지 않는 효과가 있다.

 

Promise 만들기

const promise = new Promise((resolve, reject) => {
  // promise 내부에서 비동기 상황 종료될 때, resolve 함수 호출
  // promise 내부에서 오류 상황일 때, reject 함수 호출
})

 

Promise에서는 then을 이용해 비동기 작업 이후 실행할 작업을 지정한다. 

function asyncPromiseWork() {
  ...
  return new Promise((resolve, reject) => {
  	...
    return resolve('complete')
  })
}

// then은 정상적으로 종료되었을 때의 작업을 지정
// then에 넘겨진 함수가 resolve가 호출되는 순간 실행되고 
// result에는 resolve를 호출하며 넘긴 complete가 들어있게 됨
asyncPromiseWork().then((result) => console.log(result));

 

Promise의 then내에서 다른 Promise를 return할 경우 체이닝으로 계속 연결할 수 있다.

promiseWork()
  .then((result) => {
    return promiseNextWork(result);
  })
  .then((result) => {
    return promiseNextWork(result);
  })
  .then((result) => {
    return promiseNextWork(result);
  });

 

Promise chain 중 작업이 실패했을 경우 catch로 잡을 수 있다.

promiseWork()
  .then((result) => {
    return promiseNextWork(result);
  })
  .then((result) => {
    return promiseNextWork(result);
  })
  .then((result) => {
    return promiseNextWork(result);
  })
  .catch((e) => {
    alert("에러 발생!");
  });

 

catch를 중간에 넣고 이후 작업을 연결해도 동작한다. 

catch를 넣지 않으면 promise chain 중 에러가 발생했을 때 chain이 멈추니 가급적 넣는 게 좋다.

 

성공과 실패 상관없이 호출해야 하는 코드가 있다면 finally에서 처리한다. 

promiseWork()
  .then((result) => {
    return promiseNextWork(result);
  })
  .catch((e) => {
    alert("에러 발생!");
  })
  .finally(() => {
    alert("작업 끝!");
  });

 

기존의 callback 함수를 promise 형태로 만들 수 있다. 

const delay = (delayTime) =>
  new Promise((resolve) => {
    setTimeout(resolve, delayTime);
  });

delay(5000)
  .then(() => {
    doSomething();
    return delay(3000);
  })
  .then(() => {
    console.log("comlete!");
  });

resolve 함수는 비동기 작업이 끝나는 순간에 호출하면 된다. 

 

위 예시 3의 request 함수를 promise 형태로 바꾸면 다음과 같다.

export function request(url) { // 콜백 함수들 제거
  return new Promise((resolve, reject) => { // new Promise
    const xhr = new XMLHttpRequest();
    xhr.addEventListener("load", (e) => {
      if (xhr.readyState === 4) {
        if (xhr.status === 200) {
          resolve(JSON.parse(xhr.responseText)); // resolve
        } else {
          reject(xhr.statusText); // reject
        }
      }
    });
    xhr.addEventListenr("error", (e) => reject(xhr.statusText)); // reject
    xhr.open("GET", url);
    xhr.send();
  });
}

 

Promise 처리된 delay와 조합하면 다음과 같다. 

request(`${API_END_POINT}/todos`)
  .then((data) => {
    this.setState({
      isLoading: false,
      todoList: data,
      comments: [],
    });
    return delay(5000);
  })
  .then(() => {
    console.log("complete!");
  });

 

Promise 내장 함수

1. Promise.all(iterable)

여러 promsise를 동시에 처리할 때 유용하다. 

const promise1 = delay(1000);
const promise2 = delay(2000);
const promise3 = delay(3000);

Promise.all([promise1, promise2, promise3]).then(() => {
  // promise1, promise2, promise3이 모두 처리된 이후 호출
});

 

2. Promise.race(iterable)

여러 promise 중 하나라도 resolve 혹은 reject되면 종료된다. 

function getRanNum(min, max) {
  // 랜덤 숫자 구하기
}

const promiseRaceCats = [1, 2, 3, 4, 5].map((cat) => {
  const raceTime = getRanNum(1000, 5000);
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(`${cat}번 고양이 완주!`);
      resolve(`${cat}반 고양이 승리!`);
    }, raceTime);
  });
});

// 가장 빠르게 완주한 고양이의 번호가 속한 resolve 내 문자열만 race().then()함수로 전달
Promise.race(promiseRaceCats).then((message) => consolle.log(message));

 

3. Promise.any(iterable)

여러 promise 중 하나라도 resolve되면 종료된다. 

race가 resolve/reject 상관없이 종료된다면, any는 하나가 무조건 resolve되어야 종료된다. 

function getRanNum(min, max) {
  // 랜덤 숫자 구하기
}

const promiseRaceCats = [1, 2, 3, 4, 5].map((cat) => {
  const raceTime = getRanNum(1000, 5000);
  return new Promise(resolve, (reject) => {
    if (n === 1) return reject(`${cat}번 고양이 기권!`);

    setTimeout(() => {
      console.log(`${cat}번 고양이 완주!`);
      resolve(`${cat}반 고양이 승리!`);
    }, raceTime);
  });
});

// 1번 고양이는 reject됨으로 무시
Promise.any(promiseRaceCats).then((message) => consolle.log(message));

 

4. Promise.allSettled(iterable)

여러 promise들이 성공했거나 실패했거나 상관없이 모두 이행된 경우를 처리할 수 있다.

function getRanNum(min, max) {
  // 랜덤 숫자 구하기
}

const promiseRaceCats = [1, 2, 3, 4, 5].map((cat) => {
  const raceTime = getRanNum(1000, 5000);
  return new Promise(resolve, (reject) => {
    if (n === 1) return reject(`${cat}번 고양이 기권!`);

    setTimeout(() => {
      console.log(`${cat}번 고양이 완주!`);
      resolve(`${cat}반 고양이 승리!`);
    }, raceTime);
  });
});

// 어떤 고양이가 실패하고 성공하든 마지막에 results를 출력
Promise.allSettled(promiseRaceCats).then((results) => consolle.log(results));

 

5. Promise.resolve

주어진 값으로 이행하는 Promise.then 객체를 만든다.

주어진 값이 Promise인 경우 해당 Promise가 반환된다. 

반환 타입을 무조건 promise로 맞춰줄 때 용이하다. 

 

6. Promise.reject

주어진 값으로 reject 처리된 Promise.then 객체를 만든다.

주어진 값이 Promise인 경우 해당 Promise가 반환된다.

 

async/await

Promise가 callback depth를 1단계로 줄여주지만 여전히 불편하다.

코드가 실행되다 promise가 있으면 promise가 실행되고, 안의 코드가 실행된 후 그 다음 코드가 실행되기 때문에 코드의 실행이 순차적이지 않아서 코드 가독성을 어렵게 만든다

const work = () => {
  console.log("run");

  delay(1000)
    .then(() => {
      console.log("1");
      return delay(1000);
    })
    .then(() => {
      console.log("2");
      return delay(1000);
    })
    .then(() => {
      console.log("3");
      return delay(1000);
    });

  console.log("running...");
};

work();
// run -> running... -> 1 -> 2 -> 3
// 코드의 흐름과 실행 순서가 다름

 

반면 async/await을 사용하면 promise를 동기 코드처럼 보이게 짤 수 있다.

실행은 여전히 비동기로 동작한다. 

const work = async () => {
  console.log("run");

  await delay(1000);
  console.log("1");

  await delay(1000);
  console.log("2");

  await delay(1000);
  console.log("3");
};

work();
// run -> 1 -> 2 -> 3

코드의 흐름과 실행 순서가 동일하고 코드가 간결해졌다. 

 

async 함수 선언 방식

// 1
async function asyncFunc () {
  const res = await request(...)
}

// 2
const asyncFunc = async () {
  const res = await request(...)
}

async 키워드 함수가 붙은 함수는 실행 결과를 Promise로 감싼다

 

async/await을 사용할 때 promise의 reject처럼 에러를 처리하고 싶다면 try-catch를 사용하면 된다. 

 


출처: 프로그래머스 프론트엔드 데브코스 

[Day 12] VanillaJS를 통한 자바스크립트 기본 역량 강화 I (4)

복사했습니다!