14. fetch() 함수

박은서's avatar
Jan 14, 2026
14. fetch() 함수

1. fetch() 함수

1️⃣ fetch() 란?

URL에 요청을 보내고, Promise를 반환하여 서버에서 받은 응답을 비동기적으로 처리하게 해주는 함수
fetch(url, options?)
  • url: 요청 보낼 주소 (필수)
  • options: HTTP 메서드, 헤더, 바디 등 추가 설정 (선택)

2️⃣ fetch() 인수

1) url (필수)

요청을 보낼 리소스 주소(문자열)
fetch("https://example.com/data");

2) options (선택)

요청 방식이나 헤더, 바디 등을 설정하는 객체
대표 옵션:
fetch("https://example.com/data", { method: "POST", // GET, POST, PUT, DELETE 등 headers: { // 요청 헤더 "Content-Type": "application/json" }, body: JSON.stringify({ // 요청 바디 (GET에는 보통 사용 X) name: "ChatGPT" }), mode: "cors", // cors, no-cors, same-origin credentials: "include", // 쿠키 포함 방식 cache: "no-cache", // 캐시 전략 redirect: "follow", // redirect 정책 });
옵션
역할
예시 값
method
요청 방식 지정
"GET", "POST", "PUT", "DELETE"
headers
요청 헤더 설정
{ "Content-Type": "application/json" }
body
서버에 보낼 데이터(POST 등에서 사용)
"text", JSON.stringify(obj)
mode
CORS 정책 설정
"cors", "no-cors", "same-origin"
credentials
쿠키/자격 증명 포함 여부
"omit", "same-origin", "include"
cache
캐시 사용 방식
"no-cache", "reload", "force-cache"
redirect
리다이렉트 처리 방식
"follow", "error", "manual"
referrer
요청 referrer 설정
"about:client", URL
referrerPolicy
referrer를 어떻게 보낼지 정책
"no-referrer", "strict-origin"
signal
요청 취소용 AbortSignal
AbortController().signal
keepalive
페이지 종료 후에도 요청 유지
true/false
integrity
Subresource Integrity 체크
"sha256-..."

3️⃣ fetch() 특징

1) Promise 기반

fetch()Promise를 반환
fetch("/data.json") .then(res => res.json()) .then(data => console.log(data));

2) async/await 사용 가능

async function loadData() { const res = await fetch("/data.json"); const data = await res.json(); console.log(data); }

4️⃣ 응답(Response) 처리

fetch()의 반환값은 Response 객체
fetch(url).then(response => { console.log(response.status); // 상태 코드 });
응답 데이터를 읽을 때는 다음 메서드를 사용
  • .json() — JSON 데이터
  • .text() — 문자열
  • .blob() — 파일/이미지
  • .arrayBuffer() — 바이너리
  • .formData() — 폼 데이터

5️⃣ 주의할 점

1) ❌ HTTP 오류(400, 500)를 자동으로 throw 하지 않음

const res = await fetch(url); if (!res.ok) { throw new Error("Request failed: " + res.status); }

2) ⭕ 네트워크 오류만 fetch가 reject 처리함

6️⃣ 요약

특징
설명
역할
HTTP 요청을 보내고 응답을 받는 표준 API
반환
Promise
장점
비동기 처리 쉬움, Response 데이터 다양한 형태로 변환 가능
단점
HTTP 에러 자동 throw 안 함

2. 수업 내용 정리 및 실습

1️⃣ fetch & 렌더링 동작 요약

1) HTML & JS 렌더링

  • HTML은 브라우저가 위에서 아래로 해석하며 화면을 그림
  • 동기 자바스크립트 코드가 실행 중이면 렌더링이 중단됨 (JS는 단일 스레드로 실행되기 때문)

2) fetch() 의 비동기 처리

  • fetch()즉시 Promise를 반환
  • 네트워크 작업은 Web API에서 백그라운드 수행
  • 자바스크립트 실행과 화면 렌더링은 멈추지 않고 계속 진행

3) 동기 네트워크라면?

만약 fetch가 동기 방식이었다면
  • 다운로드가 끝날 때까지 JS 실행 멈춤
  • 화면 그리기 중단
  • 사용자는 “멈춘 화면”을 보게 됨
➡️ 그래서 fetch는 비동기

4) 함수 종료 후에도 데이터를 쓸 수 있는 이유

  • fetch 호출 함수가 끝나도
  • Promise에 연결된 .then() / await나중에 실행
  • 그 시점에 Response 객체와 데이터에 접근 가능

5) Promise & Response의 관계

  • Promise는 미래의 값을 약속하는 껍데기(pending 상태)
  • 응답이 오면 Promise는 fulfilled 상태가 되고 실제 데이터는 Response 객체에 들어 있음
    • response.headers
    • response.body (스트림)
    • response.json() 호출로 본문 파싱
 

2️⃣ 실습

1) fetch() → promise(pending 상태 → fulfilled 상태)

<!DOCTYPE html> <html lang="en"> <head> <title>Document</title> <style> div { border: 1px solid black; padding: 10px; } </style> </head> <body> <div class="container"> <div id="user-1"></div> </div> <script> function download() { let res = fetch("https://jsonplaceholder.typicode.com/posts/1"); console.log(res); } download(); </script> </body> </html>
notion image

2) pending 상태에서 json 파싱하면?

<!DOCTYPE html> <html lang="en"> <head> <title>Document</title> <style> div { border: 1px solid black; padding: 10px; } </style> </head> <body> <div class="container"> <div id="user-1"></div> </div> <script> function download() { let res = fetch("https://jsonplaceholder.typicode.com/posts/1"); let resBody = res.json(); // JavaScript Object로 변환 console.log(resBody); } download(); // while (true) {} </script> </body> </html>
notion image
➡️ resBody는 아직 pending 상태이기 때문에 json으로 파싱할 데이터가 없다고 뜸!
이걸 해결해야 함 → 그게 async / await
⚠️ while (true) {} 를 넣으면 그림 그리는 게 끝나지 않기 때문에 화면에 아무것도 뜨지 않고 fetch로 돌아갈 수 없음!

3) async / await

<!DOCTYPE html> <html lang="en"> <head> <title>Document</title> <style> div { border: 1px solid black; padding: 10px; } </style> </head> <body> <div class="container"> <div id="user-1"></div> </div> <script> async function download() { let res = await fetch("https://jsonplaceholder.typicode.com/posts/1"); let resBody = await res.json(); // JavaScript Object로 변환 console.log(resBody); } download(); </script> </body> </html>
notion image
notion image

※ async / await 동작 이해하기

① async 함수란?
  • async가 붙은 함수는 항상 Promise를 반환
  • 내부 코드가 비동기 흐름을 제어할 수 있음
② await의 역할
  • await은 해당 Promise가 완료될 때까지 그 줄에서 함수 실행을 잠시 멈춤
  • 하지만 자바스크립트 전체가 멈추는 게 아니라 그 async 함수만 잠시 멈춤!
③ 실행 흐름
  1. download() 호출
  1. 함수 안에서 fetch() 실행 → 바로 Promise 반환
  1. 네트워크 요청은 **브라우저(Web API)**가 처리
  1. JS는 함수 실행을 중단하고 다음 코드 실행/렌더링(함수의 큐와 스택 모두 남겨놓은 상태)
  1. 네트워크 끝나면 Promise 상태 변경 (pending → fulfilled)
  1. 이벤트 루프가 async 함수의 대기 지점 이후 코드를 다시 실행

4) json 파싱

<!DOCTYPE html> <html lang="en"> <head> <title>Document</title> <style> div { border: 1px solid black; padding: 10px; } </style> </head> <body> <div class="container"> <div id="post-1"></div> </div> <script> async function download() { let res = await fetch("https://jsonplaceholder.typicode.com/posts/1"); console.log(res); // 통신으로 받는 데이터의 타입은 JSON!(타입 확인용 코드) const contentType = res.headers.get("content-type"); console.log(contentType); // json 파싱 let post = await res.json(); // json문자열을 javascript object로 변환 console.log(post); let dom = document.querySelector("#post-1"); dom.innerHTML = ` userId : ${post.userId} <br> id : ${post.id} <br> title : ${post.title} <br> body : ${post.body} <br> `; } download(); </script> </body> </html>
notion image
⚠️ 파싱하려면 먼저 브라우저 콘솔에서 json 내부를 확인 → 해당 json에 맞게 document.querySelector, dom.innerHTML 해야 함

5) 통신으로 받은 데이터가 여러 건일 때

<!DOCTYPE html> <html lang="en"> <head> <title>Document</title> <style> div { border: 1px solid black; padding: 10px; } .container { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px; } .post { border: 2px solid #4a90e2; border-radius: 8px; padding: 20px; background: transparent; transition: border-color 0.2s, box-shadow 0.2s; } .post:hover { border-color: #357abd; box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.1); } .meta-info { display: flex; gap: 15px; margin-bottom: 12px; font-size: 12px; color: #666; } .meta-item { background: transparent; padding: 4px 10px; border-radius: 12px; font-weight: 500; border: 1px solid #d0d0d0; } .title { font-size: 18px; font-weight: bold; color: #333; margin-bottom: 12px; line-height: 1.4; } .body { font-size: 14px; color: #555; line-height: 1.6; text-align: justify; } </style> </head> <body> <div id="post-box" class="container"></div> <script> function renderUserItem(post) { let item = document.createElement("div"); item.setAttribute("id", `post-${post.id}`); item.setAttribute("class", `post`); item.innerHTML = `<div class="meta-info"> <span class="meta-item">userId: ${post.userId}</span> <span class="meta-item">id: ${post.id}</span> </div> <div class="title"> ${post.title} </div> <div class="body"> ${post.body} </div>`; return item; } async function download() { // 1. fetch let res = await fetch("https://jsonplaceholder.typicode.com/posts"); // 2. 파싱 (json Array -> list) let posts = await res.json(); // 3. 랜더링 let dom = document.querySelector("#post-box"); posts.forEach((post) => { dom.append(renderUserItem(post)); }); } download(); </script> </body> </html>
notion image

6) 브라우저 콘솔에서 특정 ID로 삭제

notion image

7) ID로 delete (상태 관리 X)

<!DOCTYPE html> <html lang="en"> <head> <title>Document</title> <style> div { border: 1px solid black; padding: 10px; } .container { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px; } #user-1 { border: 2px solid #4a90e2; border-radius: 8px; padding: 20px; background: transparent; transition: border-color 0.2s, box-shadow 0.2s; } #user-1:hover { border-color: #357abd; box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.1); } .meta-info { display: flex; gap: 15px; margin-bottom: 12px; font-size: 12px; color: #666; } .meta-item { background: transparent; padding: 4px 10px; border-radius: 12px; font-weight: 500; border: 1px solid #d0d0d0; } .title { font-size: 18px; font-weight: bold; color: #333; margin-bottom: 12px; line-height: 1.4; } .body { font-size: 14px; color: #555; line-height: 1.6; text-align: justify; } </style> </head> <body> <div> <input type="text" id="my-input" /> <button type="button" onclick="del()">id로삭제</button> </div> <div id="post-box" class="container"></div> <script> function del() { let id = document.querySelector("#my-input").value; let delDom = document.querySelector(`#post-${id}`); delDom.remove(); } function renderUserItem(post) { let item = document.createElement("div"); item.setAttribute("id", `post-${post.id}`); item.innerHTML = `<div class="meta-info"> <span class="meta-item">userId: ${post.userId}</span> <span class="meta-item">id: ${post.id}</span> </div> <div class="title"> ${post.title} </div> <div class="body"> ${post.body} </div>`; return item; } async function download() { // 1. fetch let res = await fetch("https://jsonplaceholder.typicode.com/posts"); // 2. 파싱 (json Array -> list) let posts = await res.json(); // 3. 랜더링 let dom = document.querySelector("#post-box"); posts.forEach((post) => { dom.append(renderUserItem(post)); }); } download(); </script> </body> </html>
notion image
⚠️ 이렇게 한 번 삭제하면 새로 고침을 하지 않으면(즉, 통신을 다시 하지 않으면) 원래 상태로 돌릴 수 없음
➡️ 기존 상태를 별도의 변수로 관리하면 통신 다시 하지 않아도 원래 상태로 돌릴 수 있음
⬇️
<!DOCTYPE html> <html lang="en"> <head> <title>Document</title> <style> div { border: 1px solid black; padding: 10px; } .container { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px; } #user-1 { border: 2px solid #4a90e2; border-radius: 8px; padding: 20px; background: transparent; transition: border-color 0.2s, box-shadow 0.2s; } #user-1:hover { border-color: #357abd; box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.1); } .meta-info { display: flex; gap: 15px; margin-bottom: 12px; font-size: 12px; color: #666; } .meta-item { background: transparent; padding: 4px 10px; border-radius: 12px; font-weight: 500; border: 1px solid #d0d0d0; } .title { font-size: 18px; font-weight: bold; color: #333; margin-bottom: 12px; line-height: 1.4; } .body { font-size: 14px; color: #555; line-height: 1.6; text-align: justify; } </style> </head> <body> <div> <input type="text" id="my-input" /> <button type="button" onclick="del()">id로삭제</button> </div> <div id="post-box" class="container"></div> <script> function del() { let id = document.querySelector("#my-input").value; let delDom = document.querySelector(`#post-${id}`); delDom.remove(); } function renderUserItem(post) { let item = document.createElement("div"); item.setAttribute("id", `post-${post.id}`); item.innerHTML = `<div class="meta-info"> <span class="meta-item">userId: ${post.userId}</span> <span class="meta-item">id: ${post.id}</span> </div> <div class="title"> ${post.title} </div> <div class="body"> ${post.body} </div>`; return item; } let state; async function download() { // 1. fetch let res = await fetch("https://jsonplaceholder.typicode.com/posts"); // 2. 파싱 (json Array -> list) let posts = await res.json(); state = posts; // 3. 랜더링 let dom = document.querySelector("#post-box"); posts.forEach((post) => { dom.append(renderUserItem(post)); }); } download(); </script> </body> </html>
➡️ 장점 : 지우고자 하는 부분만 삭제 가능
➡️ 단점 : id로 삭제하면 삭제하기는 편하지만 원래 데이터로 복구가 안됨(데이터 관리가 안되기 때문)
원래 데이터 보려면 통신을 다시 해야 함

8) 상태로 관리하는 방법(검색)

<!DOCTYPE html> <html lang="en"> <head> <title>Document</title> <style> div { border: 1px solid black; padding: 10px; } .container { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px; } #user-1 { border: 2px solid #4a90e2; border-radius: 8px; padding: 20px; background: transparent; transition: border-color 0.2s, box-shadow 0.2s; } #user-1:hover { border-color: #357abd; box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.1); } .meta-info { display: flex; gap: 15px; margin-bottom: 12px; font-size: 12px; color: #666; } .meta-item { background: transparent; padding: 4px 10px; border-radius: 12px; font-weight: 500; border: 1px solid #d0d0d0; } .title { font-size: 18px; font-weight: bold; color: #333; margin-bottom: 12px; line-height: 1.4; } .body { font-size: 14px; color: #555; line-height: 1.6; text-align: justify; } </style> </head> <body> <div> <input type="text" id="my-input" /> <button type="button" onclick="search()">id로 검색</button> <button type="button" onclick="list()">전체목록</button> </div> <div id="post-box" class="container"></div> <script> let state; function list() { let dom = document.querySelector("#post-box"); dom.innerHTML = ""; state.forEach((post) => { dom.append(renderUserItem(post)); }); } function search() { let id = document.querySelector("#my-input").value; let newState = state.filter((post) => post.id == id); let dom = document.querySelector("#post-box"); dom.innerHTML = ""; newState.forEach((post) => { dom.append(renderUserItem(post)); }); } function renderUserItem(post) { let item = document.createElement("div"); item.innerHTML = `<div class="meta-info"> <span class="meta-item">userId: ${post.userId}</span> <span class="meta-item">id: ${post.id}</span> </div> <div class="title"> ${post.title} </div> <div class="body"> ${post.body} </div>`; return item; } async function download() { // 1. fetch let res = await fetch("https://jsonplaceholder.typicode.com/posts"); // 2. 파싱 (json Array -> list) let posts = await res.json(); state = posts; // 3. 랜더링 let dom = document.querySelector("#post-box"); state.forEach((post) => { dom.append(renderUserItem(post)); }); } download(); </script> </body> </html>
notion image
notion image
notion image
➡️ 단점 : 상태로 관리하면 원래 있던 상태 다 날리고 다시 그려야 한다!
나중에 리액트 쓰면 보완가능

9) 상태로 관리하는 방법(삭제)

<!DOCTYPE html> <html lang="en"> <head> <title>Document</title> <style> div { border: 1px solid black; padding: 10px; } .container { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px; } #user-1 { border: 2px solid #4a90e2; border-radius: 8px; padding: 20px; background: transparent; transition: border-color 0.2s, box-shadow 0.2s; } #user-1:hover { border-color: #357abd; box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.1); } .meta-info { display: flex; gap: 15px; margin-bottom: 12px; font-size: 12px; color: #666; } .meta-item { background: transparent; padding: 4px 10px; border-radius: 12px; font-weight: 500; border: 1px solid #d0d0d0; } .title { font-size: 18px; font-weight: bold; color: #333; margin-bottom: 12px; line-height: 1.4; } .body { font-size: 14px; color: #555; line-height: 1.6; text-align: justify; } </style> </head> <body> <div> <input type="text" id="my-input" /> <button type="button" onclick="del()">id로 삭제</button><br /> <button type="button" onclick="list()">전체목록</button><br /> </div> <div id="post-box" class="container"></div> <script> // 상태관리 let state; // 현재상태 let prev; // 최초상태 let next; // 변경된 미래 상태 // 전체목록보기 function list() { let dom = document.querySelector("#post-box"); dom.innerHTML = ""; // 현재상태 갱신 state = prev; // 최초상태 prev.forEach((post) => { dom.append(renderUserItem(post)); }); } // 삭제하기 function del() { // 값을 안받으면 공백임 let id = document.querySelector("#my-input").value; // 유효성 검사 // if (id == "") { // alert("삭제할 id를 입력하세요"); // return; // } next = state.filter((post) => post.id != id); let dom = document.querySelector("#post-box"); // if 로직으로 next 상태와 state 현재 상태를 비교해 볼 수 있음. if (next.length != state.length) { state = next; // 현재 상태 갱신 dom.innerHTML = ""; next.forEach((post) => { dom.append(renderUserItem(post)); }); } else { alert("변경된 데이터가 없어요"); } document.querySelector("#my-input").value = ""; document.querySelector("#my-input").focus(); } // Item 디자인 리턴해주는 함수 function renderUserItem(post) { let item = document.createElement("div"); item.innerHTML = `<div class="meta-info"> <span class="meta-item">userId: ${post.userId}</span> <span class="meta-item">id: ${post.id}</span> </div> <div class="title"> ${post.title} </div> <div class="body"> ${post.body} </div>`; return item; } // 데이터 다운로드 함수 (최초에 단 한번만 실행됨) async function download() { // 1. fetch let res = await fetch("https://jsonplaceholder.typicode.com/posts"); // 2. 파싱 (json Array -> list) let posts = await res.json(); prev = posts; // 최초 원본 데이터 보관 state = prev; // 현재 상태 보관 // 3. 랜더링 let dom = document.querySelector("#post-box"); state.forEach((post) => { dom.append(renderUserItem(post)); }); } // 다운로드 실행 download(); </script> </body> </html>
notion image
notion image
notion image
Share article