본문 바로가기
네트워크2

브라우저 업로드 진행률 표시 XHR / fetch :: 콜백 vs Promise, async/await

by 로맨틱스터디 2025. 10. 4.
728x90
반응형

웹에서 파일 업로드할 때 클라이언트에 **업로드 진행률(1~100%)**을 표시하려면,

브라우저가 전송하는 업로드 이벤트를 잡아서 진행률을 계산해서 보여주면 됩니다.

 

보통 두 가지 방식이 많이 쓰여요:


1. XHR(XMLHttpRequest) 방식

<input type="file" id="fileInput" />
<progress id="progressBar" value="0" max="100"></progress>
<span id="percent">0%</span>

<script>
  document.getElementById("fileInput").addEventListener("change", function () {
    const file = this.files[0];
    if (!file) return;

    const xhr = new XMLHttpRequest();
    const formData = new FormData();
    formData.append("file", file);

    // 업로드 진행률 이벤트
    xhr.upload.addEventListener("progress", function (e) {
      if (e.lengthComputable) {
        const percent = Math.round((e.loaded / e.total) * 100);
        document.getElementById("progressBar").value = percent;
        document.getElementById("percent").innerText = percent + "%";
      }
    });

    xhr.open("POST", "/upload");
    xhr.send(formData);
  });
</script>

👉 여기서 xhr.upload.onprogress 이벤트를 활용하면, 업로드 진행 상태를 클라이언트에서 실시간으로 알 수 있어요.

 

더보기

요즘은 XMLHttpRequest(XHR)를 직접 쓰는 경우가 줄었고,

대부분은 fetch API나 그 위에서 동작하는 Axios, SWR, React Query 같은 라이브러리를 많이 씁니다.

 

다만, 파일 업로드 진행률 같은 경우는 약간 애매해요:


✅ 현재 상황

  • fetch API
    표준이고 모던하지만 업로드 진행률 이벤트(onprogress)를 지원하지 않음. (다운로드 스트리밍은 지원해요)
  • XMLHttpRequest
    오래됐지만 여전히 업로드 진행률 이벤트 제공 → 그래서 아직도 파일 업로드 바에는 종종 사용됩니다.
  • Axios
    내부적으로 XHR을 써서 onUploadProgress 같은 옵션 제공.
    실무에서는 "직접 XHR 쓰는 대신 Axios"를 많이 쓰는 이유가 이거예요.

✅ fetch만으로 하고 싶다면?

업로드가 아니라 다운로드에 대해서는 fetch + ReadableStream으로 진행률 계산이 가능하지만, 업로드는 아직 공식적으로 지원이 없어요. 그래서 브라우저 표준만 쓰고 싶으면 업로드 진행률은 직접 구현하기가 까다롭습니다.


🚀 정리

  • 단순히 최신 문법만 고집 → fetch는 업로드 진행률 이벤트가 없어서 진행률 표시 불가
  • 진행률까지 필요 → Axios 같은 라이브러리를 쓰거나, 어쩔 수 없이 XMLHttpRequest 사용
  • 업로드 후 서버 처리 진행률 → WebSocket / SSE로 서버 상태를 따로 알려줘야 함

 

XMLHttpRequest(XHR)는 웹 초창기부터 AJAX 통신을 가능하게 한 핵심 API였지만,

지금은 fetch 같은 대체 기술이 나오면서 점점 덜 쓰이는 추세예요. 이유를 정리해보면:


🚫 XHR이 요즘 많이 안 쓰이는 이유

  1. 복잡하고 가독성 낮음
    • 콜백 기반이라 코드가 길어지고, 에러 처리/체이닝이 지저분해집니다.
    • 예:
const xhr = new XMLHttpRequest();
xhr.open("GET", "/api/data");
xhr.onload = function() {
  if (xhr.status === 200) {
    console.log(xhr.responseText);
  }
};
xhr.send();

 

→ 같은 걸 fetch로 하면:

fetch("/api/data")
  .then(res => res.json())
  .then(data => console.log(data));
  1. Promise/async-await 미지원
    • XHR은 자체적으로 Promise를 지원하지 않습니다.
    • fetch는 기본적으로 Promise 기반이라 async/await 문법과 자연스럽게 맞습니다.
  2. 스트리밍(Streaming) 한계
    • XHR은 응답이 전부 다 와야 responseText로 접근 가능 → 대용량/스트리밍 처리 불편.
    • fetch는 ReadableStream을 활용해서 데이터를 조각조각(streaming) 받을 수 있어 성능에 유리합니다.
  3. 인터페이스가 구식
    • 이벤트(onload, onerror, onprogress 등) 위주 API → 모던한 체이닝 스타일에 비해 불편.
    • 헤더/응답 처리도 fetch가 훨씬 직관적이고 깔끔합니다.
  4. 호환성 문제는 거의 사라짐
    • 예전에는 IE 지원 때문에 XHR이 필수였지만, 지금은 fetch가 거의 모든 최신 브라우저에서 기본 지원됩니다.

✅ 그런데도 XHR이 아직 쓰이는 경우

  • 업로드 진행률(progress) 이벤트
    → fetch는 업로드 이벤트를 제공하지 않음. 그래서 파일 업로드 시 진행률 표시가 필요하면 여전히 XHR이나 Axios(내부적으로 XHR 사용)를 씁니다.
  • 레거시 코드 유지보수
    오래된 프로젝트에는 여전히 XHR 기반 코드가 많습니다.

👉 정리하면,

  • XHR은 기능은 강력하지만 문법이 복잡하고 구식
  • fetch는 간결하고 모던한 API + Promise/async-await 지원 + 스트리밍 가능
  • 단, 업로드 진행률 같은 특정 기능 때문에 XHR이 아직 완전히 사라지진 않음

 

“콜백 기반”이라는 말은, 비동기 작업이 끝났을 때 실행할 함수 미리 넘겨주는 방식을 말해요.


📌 콜백 기반의 예시

 
const xhr = new XMLHttpRequest();
xhr.open("GET", "/api/data");

xhr.onload = function () {
  if (xhr.status === 200) {
    console.log("응답:", xhr.responseText);
  }
};

xhr.onerror = function () {
  console.error("에러 발생");
};

xhr.send();
  • getData는 1초 뒤에 데이터를 가져옴
  • 데이터가 준비되면 **콜백 함수(callback)**을 호출해서 결과를 알려줌

이런 식으로 일 끝나면 이 함수 불러줘” 하고 함수를 넘겨주는 게 콜백 기반이에요.


📌 XHR의 콜백 기반 예

 
const xhr = new XMLHttpRequest();
xhr.open("GET", "/api/data");

xhr.onload = function () {
  if (xhr.status === 200) {
    console.log("응답:", xhr.responseText);
  }
};

xhr.onerror = function () {
  console.error("에러 발생");
};

xhr.send();
  • 요청 성공하면 → onload 콜백 실행
  • 요청 실패하면 → onerror 콜백 실행

📌 문제점 (콜백 지옥)

콜백을 여러 개 중첩해서 쓰다 보면 코드가 복잡해집니다.

 
loginUser("id", "pw", function(user) {
  getUserProfile(user, function(profile) {
    getUserPosts(profile.id, function(posts) {
      console.log("결과:", posts);
    });
  });
});

→ 들여쓰기가 깊어지고, 에러 처리도 중복되면서 유지보수가 힘들어져요. 이걸 흔히 **콜백 지옥(callback hell)**이라고 부릅니다.


📌 Promise / async-await (콜백 대체)

이 문제를 해결하기 위해 등장한 게 Promiseasync/await입니다.

 
// Promise 체이닝
loginUser("id", "pw")
  .then(user => getUserProfile(user))
  .then(profile => getUserPosts(profile.id))
  .then(posts => console.log("결과:", posts))
  .catch(err => console.error(err));

// async/await
async function run() {
  try {
    const user = await loginUser("id", "pw");
    const profile = await getUserProfile(user);
    const posts = await getUserPosts(profile.id);
    console.log("결과:", posts);
  } catch (err) {
    console.error(err);
  }
}

→ 가독성이 좋아지고, 동기 코드처럼 작성할 수 있어서 훨씬 관리하기 쉽습니다.


👉 정리하면:

  • 콜백 기반 = "작업 끝나면 이 함수 불러" 라고 함수를 넘겨주는 방식
  • XHR은 이런 콜백 기반 API라서 코드가 길고 복잡해짐
  • fetch는 Promise 기반이라 async/await훨씬 깔끔하게 쓸 수 있음

프로그래밍에서 **동기(synchronous)**와 **비동기(asynchronous)**는

작업이 실행되고 결과를 기다리는 방식의 차이를 말합니다.


✅ 동기(synchronous)

  • 작업을 순서대로 하나씩 실행하는 방식
  • 앞의 작업이 끝나야 다음 작업이 실행됨
  • 마치 줄 서서 차례차례 처리되는 느낌

예시 (동기)

console.log("1. 주문 받기");
console.log("2. 음식 만들기");
console.log("3. 서빙하기");

실행 결과:

 
1. 주문 받기
2. 음식 만들기
3. 서빙하기

→ 앞에 게 끝나야 뒤에 게 실행됨.


✅ 비동기(asynchronous)

  • 작업이 끝날 때까지 기다리지 않고 다음 작업을 먼저 실행하는 방식
  • 시간이 오래 걸리는 작업(예: 서버 요청, 파일 읽기, 타이머)을 기다리는 동안 다른 일을 할 수 있음
  • "끝나면 알려줄게(콜백, Promise, async/await)" 이런 개념

예시 (비동기)

console.log("1. 주문 받기");

setTimeout(() => {
  console.log("2. 음식 만들기 (3초 걸림)");
}, 3000);

console.log("3. 서빙하기 준비");

실행 결과:

 
1. 주문 받기 3. 서빙하기 준비 2. 음식 만들기 (3초 걸림)

음식 만드는 동안(3초) 기다리지 않고 서빙 준비를 먼저 실행.
→ 음식이 다 만들어지면(3초 후) 결과가 나옴.


✅ 현실 비유

  • 동기: 은행 창구에서 내 차례가 올 때까지 줄 서서 기다림
  • 비동기: 번호표 뽑고 기다리는 동안 커피 마시러 갔다가, 내 번호가 불리면 창구로 가는 것

👉 정리:

  • 동기: "차례대로, 기다리면서 진행"
  • 비동기: "기다리지 않고, 끝나면 알려줌"

2. Fetch + Axios 같은 라이브러리 사용

만약 fetch만 쓰면 진행률 추적이 어렵지만, Axios 같은 라이브러리는 진행률 콜백을 제공합니다.

 
import axios from "axios";

const fileInput = document.getElementById("fileInput");

fileInput.addEventListener("change", async function () {
  const file = this.files[0];
  if (!file) return;

  const formData = new FormData();
  formData.append("file", file);

  await axios.post("/upload", formData, {
    onUploadProgress: (progressEvent) => {
      const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total);
      document.getElementById("progressBar").value = percent;
      document.getElementById("percent").innerText = percent + "%";
    },
  });
});

3. 서버 쪽 고려사항

  • 업로드는 실제로 서버에 데이터를 전송하는 동안 발생하는 이벤트라서, 서버는 특별히 안 해도 되지만,
    업로드 후 처리 진행 상황(예: 압축, 변환 같은 추가 작업)까지 표시하려면 WebSocket/SSE로 서버 상태를 클라이언트에 보내줘야 합니다.
  • 단순히 업로드 퍼센트만 표시할 거라면 클라이언트 측 이벤트로 충분합니다.

👉 정리:

  • 단순 업로드 전송 퍼센트: xhr.upload.onprogress or axios.onUploadProgress
  • 업로드 후 서버 작업까지 진행률: 서버 → 클라이언트에 WebSocket/SSE로 진행률 push
728x90

 

async/await는 자바스크립트에서 비동기 코드를 동기 코드처럼 읽기 쉽게 작성할 수 있게 해주는 문법이에요.


✅ 배경

  • 예전: 콜백(callback) 기반 → 중첩이 많아지고 가독성이 떨어짐 (콜백 지옥)
  • 그 후: Promise 등장 → 체이닝으로 조금 나아졌지만 .then().then().catch()가 길어질 수 있음
  • 최신: async/await 마치 순서대로 실행되는 것처럼 작성 가능

✅ 기본 문법

  1. async 함수
    • 함수 앞에 async를 붙이면, 그 함수는 항상 Promise를 반환합니다.
async function myFunc() {
  return 42;
}
myFunc().then(result => console.log(result)); // 42
더보기

📌 Promise가 뭔가요?

 

Promise는 자바스크립트에서 비동기 작업의 **“결과를 담을 약속 객체”**입니다.

 

즉, 지금은 결과가 없지만,

미래에 **성공하면 값(resolve), 실패하면 이유(reject)**를 알려주는 상자라고 생각하면 돼요.

 

 

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("성공!");
    // reject("실패...");  // 실패하는 경우
  }, 1000);
});

promise.then(result => console.log(result)); // 1초 뒤 "성공!"

📌 async 함수가 Promise를 반환한다는 의미

 
async function myFunc() {
  return 42;
}
  • 여기서 return 42;는 그냥 숫자 42를 반환하는 것처럼 보이지만,
  • async가 붙어 있기 때문에 자동으로 Promise.resolve(42) 형태로 감싸져서 반환됩니다.

즉, 내부적으로는 이런 거랑 같아요:

function myFunc() {
  return Promise.resolve(42);
}

📌 그래서 왜 .then()을 쓸 수 있나?

 
myFunc().then(result => console.log(result));
  • myFunc()는 사실 Promise를 반환합니다. (Promise { 42 })
  • 그래서 .then()을 붙여서, 결과값(42)이 준비됐을 때 사용할 수 있는 거예요.

📌 직접 확인해보기

 
async function myFunc() {
  return 42;
}

const result = myFunc();
console.log(result); // Promise { 42 }

👉 콘솔에 찍어보면 Promise 객체가 나옵니다. 숫자 42 자체가 아니라, 42를 담고 있는 Promise예요.


📌 정리

  • async 함수는 항상 Promise를 반환합니다.
  • return 42;라고 써도 실제로는 Promise.resolve(42)를 반환하는 것과 같음.
  • 그래서 myFunc() 호출하면 결과는 42가 아니라 Promise { 42 }.
  • .then()이나 await를 써야 실제 값 42를 꺼낼 수 있습니다.

 

**Promise는 “결과값을 나중에 줄게”라는 약속(상자)**이고,
.then()이나 await은 그 상자를 열어서 실제 값을 꺼내는 방법입니다.


1. .then() 으로 꺼내기

 
async function myFunc() {
  return 42;
}

myFunc().then(result => {
  console.log(result); // 👉 42
});
  • myFunc() 실행Promise { 42 } 반환
  • .then() 안의 함수Promise가 성공(resolve) 되었을 때 실행
  • 그래서 result에는 진짜 값 42가 들어옴

2. await 으로 꺼내기

 
async function myFunc() {
  return 42;
}

async function run() {
  const result = await myFunc();
  console.log(result); // 👉 42
}

run();
  • await myFunc()myFunc()가 반환한 Promise가 끝날 때까지 기다린 뒤 실제 값 42를 반환
  • 그래서 result 변수에 바로 42가 들어감
  • 단, await은 async 함수 안에서만 사용 가능

3. 직접 눈으로 확인하기

 
async function myFunc() {
  return 42;
}

console.log(myFunc()); 
// 👉 Promise { 42 }  (그냥 호출하면 Promise 상자)

myFunc().then(value => console.log(value));
// 👉 42  (.then()으로 값 꺼내기)

async function run() {
  const value = await myFunc();
  console.log(value);
}
// 👉 42  (await으로 값 꺼내기)
run();

🚚 택배 박스 비유

  • Promise { 42 } = "안에 42라는 물건이 들어있는 택배 상자, 하지만 아직 도착 중일 수도 있음"
  • .then() = "택배가 도착하면 열어서 안에 있는 물건(42)을 꺼내 쓰겠다"
  • await = "택배가 도착할 때까지 잠깐 기다렸다가, 물건(42)을 꺼내서 바로 쓰겠다"

👉 정리하면:

  • myFunc() 자체는 Promise를 반환 → 그냥은 값 못 씀
  • .then() 이나 await을 통해 Promise가 resolve된 후 실제 값(42)을 꺼낼 수 있음

 

지금까지 본 async/await / Promise 예제를 콜백(callback) 방식으로 바꿔서 표현해드릴게요.


📌 원래 async/await 코드

 
async function myFunc() {
  return 42;
}

async function run() {
  const result = await myFunc();
  console.log(result); // 42
}

run();

📌 같은 동작을 콜백으로 구현하기

콜백 기반에서는 Promise 대신 **“작업이 끝나면 실행할 함수”**를 직접 넘겨야 해요.

 
function myFunc(callback) {
  // 비동기 작업 흉내내기 (예: 서버 응답)
  setTimeout(() => {
    callback(42); // 일이 끝나면 callback 실행, 결과 전달
  }, 1000);
}

function run() {
  myFunc(function(result) {
    console.log(result); // 42
  });
}

run();

📌 차이점

  • 콜백 방식
    • myFunc(callback) 호출 시, 바로 결과를 반환하지 않고
    • 일이 끝나면 내가 넘긴 callback 함수를 불러줌
    • 그래서 결과는 callback 안에서만 사용할 수 있음
  • Promise/async/await 방식
    • myFunc()가 Promise { 42 }라는 “결과 상자”를 반환
    • .then()이나 await으로 결과를 꺼낼 수 있음
    • 코드가 동기적으로 읽혀서 훨씬 깔끔함

📌 흐름 비교

콜백

 
myFunc(callback)
 └──> setTimeout 1초 후 → callback(42)

Promise/await

 
myFunc() → Promise { 42 }
await myFunc() → Promise resolve → 42

👉 정리:

  • callback = “일 끝나면 내가 준 함수를 실행해줘”
  • Promise/await = “결과가 담긴 상자(Promise)를 받고, 다 끝나면 꺼내 쓰자”

 

콜백 기반 예제에서 setTimeout(() => { callback(42); }, 1000)에서 말하는 **“일이 끝났다”**는

바로 1초 기다리는 것이에요.


하나씩 풀어보면

 
function myFunc(callback) {
  setTimeout(() => {
    callback(42); // 1초 후 결과 전달
  }, 1000);
}
  1. myFunc가 호출되면
    • 바로 결과(42)를 반환하지 않고, 내부에서 setTimeout을 걸어요.
    • setTimeout은 1초 후 실행되도록 예약만 해놓는 함수예요.
  2. 1초 동안 자바스크립트는 다른 코드 실행 가능
    • 이게 바로 비동기(asynchronous) 특성입니다.
    • 즉, 기다리는 동안 UI가 멈추지 않고 다른 작업도 할 수 있음
  3. 1초 후 setTimeout 내부 코드 실행
    • 여기서 **“일이 끝났다”**라고 보면 됨
    • 그러면 우리가 미리 넘겨둔 callback 함수가 호출되고, 결과 42를 받음
myFunc(function(result) {
  console.log(result); // 42 출력
});
  • 1초 후에 콘솔에 42가 출력되는 이유는
    1초 기다린 뒤 callback 함수 실행되었기 때문이에요.

요약

  • 콜백에서 말하는 “일이 끝났다” = 비동기 작업이 완료된 시점
  • 예제에서는 **1초 기다림(setTimeout)**이 그 비동기 작업
  • 실제 서버 요청, 파일 읽기, 업로드 등도 동일한 원리로,
    응답이 오면 콜백을 호출”하는 구조예요

 

  1. await 키워드
    • async 함수 안에서만 사용 가능
    • Promise가 처리될 때까지 기다렸다가, 결과값을 반환
    • 실행 흐름은 잠깐 "멈춘 것처럼" 보이지만, 실제로는 다른 작업은 계속 돌아감 (비동기 유지
     
    async function getData() {
      const response = await fetch("https://jsonplaceholder.typicode.com/posts/1");
      const data = await response.json();
      console.log(data);
    }
    getData();

✅ Promise vs async/await 비교

Promise 스타일

 
fetch("https://jsonplaceholder.typicode.com/posts/1")
  .then(response => response.json())
  .then(data => {
    console.log("데이터:", data);
  })
  .catch(err => console.error(err));

async/await 스타일

 
async function loadPost() {
  try {
    const response = await fetch("https://jsonplaceholder.typicode.com/posts/1");
    const data = await response.json();
    console.log("데이터:", data);
  } catch (err) {
    console.error(err);
  }
}
loadPost();

→ 기능은 같지만, 동기 코드처럼 읽히고 가독성이 훨씬 좋아짐


✅ 장점

  • 동기 코드처럼 직관적이라 가독성 ↑
  • try/catch로 에러 처리 가능
  • 콜백 지옥/Promise 체이닝 문제 해결

✅ 단점

  • 병렬 실행할 때는 Promise.all() 같은 걸 활용해야 더 효율적
 
// 순차 실행 (비효율적)
const a = await fetch("/a");
const b = await fetch("/b");

// 병렬 실행 (효율적)
const [a, b] = await Promise.all([fetch("/a"), fetch("/b")]);

👉 정리하면:

  • async = 함수가 Promise 반환하게 만듦
  • await = Promise 결과 나올 때까지 기다림
  • 결과적으로 비동기 코드를 동기처럼 깔끔하게 작성할 수 있음
728x90
반응형