JavaScript

비동기 처리 & 모듈

1. 동기 vs 비동기

  • 동기(Synchronous): 한 작업이 끝나야 다음 작업이 시작된다. 코드가 작성된 순서대로 실행된다.

  • 비동기(Asynchronous): 시간이 걸리는 작업(네트워크 요청, 타이머 등)을 기다리지 않고 다음 코드를 먼저 실행한다. 작업이 끝나면 나중에 그 결과를 처리한다.

console.log("First");
setTimeout(() => {
  console.log("Second");   // 1초 뒤에 실행됨
}, 1000);
console.log("Third");

First Third // (1초 후) Second

setTimeout이 1초를 기다리는 동안 멈추지 않고 Third를 먼저 출력한다. 이것이 비동기의 핵심이다.

flowchart LR
    A["console.log('First')"] --> B["setTimeout 예약<br/>(1초 후 실행)"]
    B --> C["console.log('Third')"]
    C -.1초 후.-> D["console.log('Second')"]
    style B fill:#fff3e0,stroke:#fb8c00
    style D fill:#e3f2fd,stroke:#1e88e5

왜 비동기가 필요한가?

서버에서 데이터를 받아오는 데 3초가 걸린다면, 동기 방식은 그 3초 동안 화면 전체가 멈춰 버린다. 비동기 방식은 기다리는 동안에도 페이지가 응답하므로 사용자 경험이 좋다.


2. 콜백 함수와 콜백 헬

비동기 작업의 결과는 콜백 함수로 처리할 수 있다. "작업이 끝나면 이 함수를 실행해 줘"라고 넘기는 방식이다.

function fetchData(callback) {
  setTimeout(() => {
    callback("받아온 데이터");
  }, 1000);
}

fetchData((data) => {
  console.log(data);   // (1초 후) "받아온 데이터"
});

그런데 여러 비동기 작업을 순서대로 처리하려고 콜백을 계속 중첩하면, 코드가 오른쪽으로 깊어지며 읽기 어려워진다. 이를 콜백 헬(Callback Hell)이라 부른다.

firstTask(() => {
  secondTask(() => {
    thirdTask(() => {
      console.log("모든 작업 완료");   // 😱 점점 깊어짐
    });
  });
});

이 문제를 해결하기 위해 등장한 것이 Promise와 async/await다.


3. Promise

Promise는 비동기 작업의 미래 결과를 담는 객체다. 세 가지 상태를 가진다.

flowchart LR
    P["⏳ 대기<br/>(pending)"] -->|성공 resolve| F["✅ 이행<br/>(fulfilled)"]
    P -->|실패 reject| R["❌ 거부<br/>(rejected)"]
    F --> T[".then()"]
    R --> C[".catch()"]
    style F fill:#e8f5e9,stroke:#43a047
    style R fill:#ffebee,stroke:#e53935

Promise 생성과 사용

Promise 생성자는 resolve(성공), reject(실패) 두 함수를 인수로 받는 콜백을 받는다.

const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    const success = true;
    if (success) {
      resolve("성공!");   // 성공 시
    } else {
      reject("실패!");    // 실패 시
    }
  }, 1000);
});

myPromise
  .then((result) => console.log("결과:", result))   // 성공 처리
  .catch((error) => console.log("오류:", error))    // 실패 처리
  .finally(() => console.log("작업 종료"));          // 항상 실행

// (1초 후) 결과: 성공! 작업 종료

메서드

역할

.then()

성공(이행)했을 때 실행

.catch()

실패(거부)했을 때 실행

.finally()

성공/실패와 관계없이 항상 마지막에 실행


4. async / await

Promise의 .then() 체인을 동기 코드처럼 읽기 쉽게 쓰는 문법이다.

  • async: 함수 앞에 붙이면 그 함수는 항상 Promise를 반환한다.

  • await: Promise가 끝날 때까지 기다렸다가 결과를 꺼낸다. async 함수 안에서만 쓸 수 있다.

function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function run() {
  console.log("Start");
  await delay(1000);   // 1초 기다림 (멈춘 것처럼 보이지만 비동기)
  console.log("1초 후 실행");
}

run();

Start // (1초 후) 1초 후 실행

try...catch로 예외 처리

await가 실패하면 try...catch로 오류를 잡을 수 있다.

async function fetchData() {
  try {
    const response = await fetch("https://api.example.com/data");
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error("오류 발생:", error);
  }
}

콜백 → Promise → async/await

같은 비동기 처리를 세 방식으로 쓸 수 있지만, async/await가 가장 읽기 쉽고 오류 처리도 직관적이라 오늘날 가장 널리 쓰인다.


5. fetch — 서버와 데이터 주고받기

fetch는 네트워크 요청을 보내는 내장 함수다. Promise를 반환하므로 .then()이나 async/await와 함께 쓴다. 기본은 GET 요청이다.

// .then 방식
fetch("https://api.example.com/data")
  .then((response) => response.json())   // 응답을 JSON으로 변환
  .then((data) => console.log(data))
  .catch((error) => console.error("오류:", error));
// async/await 방식 (권장)
async function getData() {
  try {
    const response = await fetch("https://api.example.com/data");
    if (!response.ok) {
      throw new Error("네트워크 응답 오류");
    }
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error("오류:", error);
  }
}

옵션 지정 (POST 등)

두 번째 인수로 메서드·헤더·본문 등을 지정한다.

fetch("https://api.example.com/data", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "철수", age: 25 })
})
  .then((res) => res.json())
  .then((data) => console.log(data));

응답 메서드

변환 형식

response.json()

JSON 객체

response.text()

문자열

response.blob()

이진 데이터(Blob)

응답 성공 여부 확인

fetch는 네트워크 자체가 실패할 때만 catch로 빠진다. 404·500 같은 HTTP 오류는 catch로 잡히지 않으므로, response.ok를 직접 확인해야 한다.


6. 모듈 (Module)

애플리케이션이 커지면 코드를 여러 파일로 나누게 된다. 이렇게 분리된 파일 하나하나를 모듈(module)이라 한다.

모듈의 장점

  • 유지보수: 기능이 분리되어 수정·개선이 쉽다.

  • 네임스페이스: 파일마다 독립된 범위를 가져 변수 이름 충돌을 막는다.

  • 재사용: 필요한 곳에서 가져다 다시 쓸 수 있다.

ES Module (표준 방식)

<script>에 type="module"을 붙이면 그 파일은 모듈로 동작한다. export로 내보내고 import로 가져온다.

<script type="module" src="index.js"></script>
// math.js — 내보내기
export const PI = 3.14;
export const add = (a, b) => a + b;
export const avg = (a, b) => (a + b) / 2;
// index.js — 가져오기
import { PI, add, avg } from "./math.js";

console.log(PI);          // 3.14
console.log(add(2, 3));   // 5

3.14 5

export default로는 모듈당 하나의 기본 값을 내보낼 수 있다.

// math.js
export default { add, avg };

// index.js — 이름은 자유롭게 지정
import math from "./math.js";
console.log(math.add(2, 3));   // 5

CommonJS (Node.js 방식, 참고)

Node.js에서 전통적으로 쓰던 방식으로, require로 가져오고 module.exports로 내보낸다.

// math.js
module.exports = { add, avg };

// index.js
const math = require("./math.js");
console.log(math.add(2, 3));

ES Module vs CommonJS

브라우저와 최신 표준은 ES Module(import/export)을, Node.js의 전통 방식은 CommonJS(require/module.exports)를 쓴다. Node.js 13.2부터는 package.json에 "type": "module"을 선언하면 Node.js에서도 ES Module을 바로 사용할 수 있다.


🎉 축하합니다! HTML · CSS · JavaScript 기초 과정을 모두 마쳤습니다. 이제 배운 내용을 조합해 직접 동작하는 웹 페이지를 만들어 보세요.

마지막 수정 2026년 10월 22일