문서 읽는 데 50분 · D3

D-3: Fetch와 비동기 데이터

전체 23강 중 21강 · HTML·CSS·JS
난이도 · 입문

안녕하세요, 홍순구 튜터입니다. 지난 시간(D-2)에 우리는 멈춰 있던 화면에 "반응"을 달았어요. 하트를 클릭하면 좋아요가 켜지고, 댓글을 쓰면 한 줄 추가되고, 삭제 버튼을 누르면 사라졌죠. 콘솔을 두드리던 함수들이 진짜 클릭과 제출에 연결되면서, 인스타 클론이 처음으로 사용자 손에 살아났어요.

그런데 마지막에 아쉬운 점이 하나 남았어요. 오늘 추가한 댓글도, 토글한 좋아요도 새로고침하면 싹 사라져요. 게다가 게시물 자체가 feed.html 안에 글자로 딱 고정돼 있어요. 진짜 인스타라면 게시물이 서버에서 계속 새로 내려와야 하는데 말이죠.

오늘 그 벽을 넘어요. fetch 라는 도구로 서버에서 진짜 데이터를 받아 와 화면에 그리는 거예요. 우리 컴퓨터에 작은 가짜 서버를 띄워서, 마치 진짜 서버처럼 게시물 목록을 주고받아요. 고정돼 있던 화면이 살아 있는 데이터로 채워지는 순간이에요.

   지난 시간 (D-2)                          오늘 (D-3)
   ┌──────────────────────────┐          ┌──────────────────────────┐
   │ 게시물이 HTML 에 글자로 고정 │   ──▶    │ 서버에서 fetch 로 받아 옴   │
   │ 데이터 = 손으로 쓴 마크업    │          │ 데이터 = 서버가 주는 JSON  │
   │ 화면과 데이터가 한 덩어리    │          │ 데이터로 화면을 그림        │
   └──────────────────────────┘          └──────────────────────────┘

💡 오늘 수업의 핵심 — "브라우저(클라이언트)가 서버에 데이터를 요청하면, 서버가 JSON으로 응답한다. fetch로 그 데이터를 받아 와, 데이터 하나하나를 게시물 화면으로 그린다." 🎯

🎯 학습 목표

  • 클라이언트와 서버가 무엇인지, HTTP 요청/응답이 어떻게 오가는지 이해합니다.
  • 데이터를 주고받는 표준 형식인 JSON과, 요청 종류를 나누는 REST API(GET/POST/PUT/DELETE)를 이해합니다.
  • 내 컴퓨터에 가짜 서버(json-server)를 띄워 게시물 데이터를 제공합니다.
  • fetch로 서버에 GET 요청을 보내고, 응답을 받아 옵니다.
  • 지난 모듈에서 배운 Promise·async/await를 fetch에 직접 적용합니다.
  • 받아 온 데이터 하나하나를 <article> 게시물로 그리는 함수를 만듭니다.
  • 서버가 응답하지 않을 때를 대비해 에러를 처리하는 기초를 익힙니다.

Step 1: "내 화면 밖의 세계" — 클라이언트와 서버, HTTP

fetch를 배우기 전에, 먼저 서버가 뭔지부터 잡고 갈게요. 이 개념 없이 fetch를 쓰면 "주문은 했는데 누구한테 한 건지 모르는" 상태가 되거든요.

지금까지 우리가 만진 건 전부 브라우저 안이었어요. HTML·CSS·JavaScript 파일을 브라우저가 읽어서 화면을 그렸죠. 이렇게 사용자 눈앞에서 화면을 그리고 클릭에 반응하는 쪽을 클라이언트(client, 손님) 라고 해요. 우리가 지금까지 만든 게 전부 클라이언트예요.

그런데 진짜 인스타의 게시물·댓글·좋아요 같은 데이터는 내 컴퓨터에 없어요. 어딘가 멀리 있는 다른 컴퓨터에 저장돼 있죠. 그 데이터를 보관하고, 요청하면 꺼내 주는 쪽을 서버(server, 제공하는 쪽) 라고 해요. 손님(클라이언트)이 주문하면 음식을 내주는 식당 주방이라고 생각하면 돼요.

       클라이언트 (브라우저)                       서버 (다른 컴퓨터)
       ┌────────────────┐                      ┌────────────────┐
       │  "게시물 목록 줘" │  ── 요청(request) ──▶ │  데이터 보관 중   │
       │                │                      │                │
       │  받아서 화면에   │  ◀─ 응답(response) ── │  "여기 JSON 줄게" │
       │  게시물을 그림   │                      │                │
       └────────────────┘                      └────────────────┘
            손님(주문)                               주방(요리 내줌)

이렇게 클라이언트와 서버가 주고받는 약속을 HTTP(HyperText Transfer Protocol) 라고 해요. 이름은 거창하지만, 핵심은 단순해요. 클라이언트가 요청(request)을 보내면, 서버가 응답(response)을 돌려준다. 이 한 번의 주고받기가 HTTP의 전부예요.

식당에 비유하면 이래요. 손님이 "아메리카노 한 잔이요"라고 요청하면, 주방이 커피를 만들어 응답으로 내주죠. 손님은 커피가 어떻게 만들어지는지 몰라도 돼요. 그냥 주문하고 받으면 돼요. fetch도 똑같아요. "게시물 목록 줘"라고 요청하면, 서버가 데이터를 응답으로 내줘요. 우리는 그걸 받아 쓰기만 하면 돼요.

💡 사용자 눈앞에서 화면을 그리는 쪽이 클라이언트(브라우저), 데이터를 보관하고 요청에 응답하는 쪽이 서버예요. 둘은 HTTP라는 약속으로 "요청 → 응답"을 주고받아요. 식당에서 손님이 주문하고 주방이 내주는 것과 같아요.


Step 2: "데이터의 모양, JSON" — REST API 4동사

서버와 데이터를 주고받는다고 했는데, 그 데이터는 무슨 모양으로 오갈까요? 사람이 보기에도, 컴퓨터가 읽기에도 편한 공통 형식이 필요해요. 그게 바로 JSON(JavaScript Object Notation) 이에요.

JSON은 이름 그대로 자바스크립트 객체와 똑 닮았어요. C-4에서 배운 객체 기억하시죠? 중괄호 {} 안에 "key": value 쌍을 적고, 여러 개는 배열 []로 묶어요. 게시물 하나를 JSON으로 적으면 이래요.

{
  "id": 1,
  "username": "jaehoon",
  "likes": 1240,
  "caption": "오늘의 일상 — 제주도 협재 해변"
}

거의 자바스크립트 객체와 똑같죠? 딱 하나 다른 점은, JSON에서는 key를 반드시 큰따옴표로 감싼다는 거예요("id"처럼요). 이 형식이 전 세계 서버와 클라이언트가 공유하는 공통 언어예요. 우리 게시물 데이터를 이 JSON 형식으로 파일에 담을 거예요. mock/db.json이라는 파일을 만들고, 게시물 배열을 적어요.

// instagram-clone-frontend/mock/db.json (앞부분 발췌 — 실제론 게시물 8개)
{
  "posts": [
    {
      "id": 1,
      "username": "jaehoon",
      "avatar": "https://picsum.photos/seed/avatar1/64/64",
      "time": "5시간",
      "image": "https://picsum.photos/seed/insta1/600/600",
      "alt": "석양이 지는 제주도 협재 해변",
      "likes": 1240,
      "caption": "오늘의 일상 — 제주도 협재 해변. 노을이 정말 예술이었어요 #sunset #travel",
      "commentCount": 84
    }
  ]
}

posts라는 이름표 아래에 게시물 객체들을 배열로 담았어요. 게시물 하나하나가 지난 시간까지 HTML에 손으로 적던 정보(작성자·이미지·좋아요 수·캡션)를 그대로 담고 있죠. 이제 이 데이터가 화면과 분리됐어요. 화면 틀은 그대로 두고, 데이터만 이 파일에서 갈아끼울 수 있게 된 거예요.

그런데 서버에 보내는 요청에도 종류가 있어요. "데이터를 달라"는 요청과 "새로 저장해 달라"는 요청은 다르잖아요. 이 요청 종류를 정해둔 약속이 REST API예요. 핵심은 네 가지 동사예요.

동사 하는 일 우리 예시
GET 데이터를 조회(가져오기) 게시물 목록 받아 오기
POST 데이터를 생성(새로 저장) 새 댓글 달기
PUT 데이터를 수정 게시물 내용 고치기
DELETE 데이터를 삭제 게시물 지우기

오늘 우리가 쓸 건 GET 하나예요. "게시물 목록을 가져온다"는 조회니까요. 나머지 POST·PUT·DELETE(저장·수정·삭제)는 다음 시간(D-4)에 댓글을 서버에 진짜 저장할 때 본격적으로 써요. 오늘은 "받아 오기(GET)"에만 집중해요.

💡 서버와 데이터를 주고받는 공통 형식이 JSON이에요. 자바스크립트 객체와 닮았는데 key를 큰따옴표로 감싸요. 요청 종류는 REST API의 4동사(GET 조회 / POST 생성 / PUT 수정 / DELETE 삭제)로 나뉘고, 오늘은 GET만 써요.


Step 3: "내 컴퓨터에 가짜 서버를" — json-server 띄우기

fetch로 데이터를 받으려면, 받을 서버가 먼저 떠 있어야 해요. 그런데 우리는 진짜 서버를 만들 줄 아직 몰라요(그건 백엔드 영역이에요). 대신 방금 만든 db.json 파일을 진짜 서버처럼 보이게 해주는 편리한 도구가 있어요. json-server예요.

json-server는 JSON 파일 하나만 주면, 그걸 서버처럼 띄워서 GET 요청에 데이터를 응답해줘요. 진짜 서버를 흉내 내는 거라 Mock(목, 가짜) 서버라고 불러요. 설치도 따로 필요 없어요. 터미널에서 이 한 줄만 치면 돼요.

npx json-server mock/db.json --port 3001

npx는 "이 도구를 잠깐 받아서 바로 실행해줘"라는 명령이에요. mock/db.json은 서버로 띄울 파일, --port 3001은 "3001번 문으로 서비스해줘"라는 뜻이에요. 실행하면 터미널에 이런 안내가 떠요. http://localhost:3001/posts 같은 주소(엔드포인트)가 생겼다고요.

⚠️ 한 가지 함정이 있어요. 검색하다 보면 npx json-server@1 ...처럼 버전을 붙인 명령을 볼 수 있는데, 이렇게 치면 "그런 버전 없음" 오류가 나요. 그냥 버전 없이 npx json-server mock/db.json --port 3001로 치세요. 그게 가장 최신 버전을 받아 줘요. 처음 한 번은 도구를 받느라 몇 초 걸릴 수 있어요.

여기서 중요한 게 있어요. 이 json-server는 계속 켜져 있어야 해요. 터미널을 끄면 서버도 꺼지죠. 그래서 우리는 터미널이 두 개 필요해요.

  터미널 1 (서버 담당)              터미널 2 / VS Code (화면 담당)
  ┌──────────────────────┐        ┌──────────────────────┐
  │ npx json-server ...   │        │ Live Server 로        │
  │ → 3001 포트에서 대기   │  ◀──▶  │   feed.html 열기      │
  │ (켜둔 채로 둠)         │        │ → fetch 로 3001 에 요청 │
  └──────────────────────┘        └──────────────────────┘

서버가 떴는지 눈으로 확인해볼게요. json-server를 켠 채로, 브라우저 주소창에 http://localhost:3001/posts를 직접 쳐보세요. 우리가 db.json에 적은 게시물 배열이 JSON 그대로 화면에 좌르륵 나와요. 아직 fetch 코드는 한 줄도 안 썼는데, 서버는 벌써 데이터를 내줄 준비가 된 거예요. 이게 "서버가 응답한다"의 실제 모습이에요.

💡 json-server는 JSON 파일을 진짜 서버처럼 띄워주는 가짜(Mock) 서버예요. npx json-server mock/db.json --port 3001로 켜고, 켜둔 채로 둬요. 브라우저로 http://localhost:3001/posts에 접속하면 데이터가 그대로 보여요.


Step 4: "fetch 첫 발걸음" — 서버에 요청 보내기

서버가 떴으니, 이제 클라이언트에서 그 데이터를 받아 올 차례예요. 그 도구가 바로 fetch 예요. fetch는 영어로 "가져오다"라는 뜻이에요. 이름 그대로 서버에서 데이터를 가져와요.

그런데 한 가지 중요한 사실이 있어요. 서버에 요청을 보내고 응답이 오기까지는 시간이 걸려요. 네트워크를 타고 멀리 다녀와야 하니까요. 짧으면 몇십 밀리초, 길면 몇 초까지도요. 그동안 브라우저가 멈춰서 기다리면 화면이 얼어붙겠죠. 그래서 fetch는 C-6에서 배운 Promise를 돌려줘요. "지금 당장은 결과가 없지만, 다녀오면 알려줄게"라는 약속표 말이에요.

콘솔에서 먼저 맛보기로 써볼게요. C-6에서 배운 .then()으로 응답을 이어받아요.

// (콘솔에서 직접 쳐보는 맛보기 — 파일 최종본 아님)
fetch("http://localhost:3001/posts")        // 1) 서버에 GET 요청
  .then((response) => response.json())       // 2) 응답을 JSON 으로 풀기
  .then((posts) => console.log(posts));      // 3) 게시물 배열을 콘솔에

세 줄을 따라가 볼게요. 먼저 fetch("...주소...")로 서버에 요청을 보내요. 주소만 적고 다른 옵션이 없으면 기본이 GET 요청이에요(조회죠). 이게 Promise를 돌려줘요.

그 Promise가 풀리면 .then으로 응답 객체(response) 를 받아요. 그런데 이 응답엔 데이터 말고도 상태 정보가 잔뜩 들어 있어서, 우리가 원하는 JSON 데이터만 꺼내려면 response.json()을 한 번 더 불러야 해요. 그런데 이 .json()도 시간이 걸리는 작업이라 또 Promise를 돌려줘요. 그래서 .then을 한 번 더 이어 붙여서, 최종적으로 게시물 배열(posts)을 받아 콘솔에 찍어요.

feed.html을 Live Server로 열고(json-server도 켜둔 채로요), 콘솔(F12)에 이 세 줄을 쳐보세요. db.json에 적은 게시물 8개가 배열로 콘솔에 찍혀요. 우리 코드가 서버에 요청을 보내고, 서버가 응답으로 데이터를 내준 거예요. 드디어 클라이언트와 서버가 처음으로 대화한 순간이에요.

💡 fetch("주소")는 서버에 GET 요청을 보내고 Promise를 돌려줘요. 응답이 오면 .then으로 받고, response.json()으로 본문을 JSON으로 풀어요. 시간이 걸리는 작업이라 Promise·.then이 쓰이는 거예요(C-6에서 배운 그 약속표예요).


Step 5: "async/await로 다시" — 같은 동작, 깔끔한 문법

방금 .then을 두 번 이어 붙였죠. 동작은 잘 하는데, .then이 줄줄이 이어지면 읽기가 조금 답답해요. 여기서 C-7에서 배운 async/await가 빛을 발해요. 기억하시죠? async/await는 Promise를 "기다렸다가 값을 꺼내는" 더 읽기 쉬운 문법이었어요.

같은 fetch를 async/await로 다시 쓰면 이래요. 이건 콘솔 맛보기가 아니라 우리 파일에 진짜 들어가는 코드예요. js/api.js라는 새 파일에 담아요.

// instagram-clone-frontend/js/api.js
const BASE_URL = "http://localhost:3001";

// 게시물 목록을 서버에서 가져와요.
// async/await 로 "응답을 기다렸다가" 결과를 돌려줘요. (C-7 에서 배운 그 문법이에요)
export async function fetchPosts() {
  const response = await fetch(`${BASE_URL}/posts`); // 1) 서버에 GET 요청을 보내요
  const posts = await response.json();               // 2) 응답 본문을 JSON 으로 풀어요
  return posts;                                      // 3) 게시물 배열을 돌려줘요
}

Step 4의 .then 두 줄이 어떻게 바뀌었는지 보세요. .then((response) => ...)const response = await fetch(...)가 됐어요. await는 "이 Promise가 풀릴 때까지 기다렸다가, 풀린 값을 꺼내서 변수에 담아라"는 뜻이에요. .then의 콜백 없이, 마치 일반 코드처럼 위에서 아래로 읽혀요.

await를 쓰려면 함수 앞에 async를 붙여야 한다는 약속도 기억하시죠(C-7에서 배웠어요). 그래서 async function fetchPosts()예요. 맨 앞 BASE_URL은 서버 주소를 한 곳에 모아둔 거예요. 나중에 주소가 바뀌어도 여기 한 줄만 고치면 되니까요. 그리고 export를 붙여서, 다른 파일(feed.js)에서 이 함수를 가져다 쓸 수 있게 했어요(C-5 모듈이에요).

이제 fetchPosts()를 부르면, 서버에서 게시물 배열을 받아 돌려줘요. .then 버전과 결과는 완전히 똑같고, 읽기만 더 편해졌어요. C-6에서 C-7로 넘어올 때 "같은 Promise, 더 읽기 쉬운 문법"을 배웠는데, 그게 fetch에서 그대로 재현된 거예요.

💡 .then 체인을 async/await로 바꾸면 같은 동작이 위에서 아래로 읽히는 평범한 코드처럼 돼요. const x = await fetch(...)로 응답을 받고, await response.json()으로 본문을 풀어요. await를 쓰니 함수 앞에 async를 붙여요.


Step 6: "데이터로 게시물을 그리다" — renderPost

이제 데이터는 받아 올 수 있게 됐어요. 그런데 데이터는 글자(JSON)일 뿐이에요. 이걸 사람이 보는 게시물 화면으로 바꿔야죠. 즉 "객체 하나 → <article> 한 채"로 그려주는 함수가 필요해요. 이 함수를 renderPost라고 부를게요(render는 "그리다"라는 뜻이에요).

다행히 우리는 이미 D-1에서 화면에 요소를 만드는 도구를 배웠어요. document.createElement로 빈 요소를 만들고, innerHTML로 그 안을 채웠죠. renderPost는 그 도구를 그대로 써요.

// instagram-clone-frontend/js/feed.js
// 게시물 데이터(객체) 하나를 받아 <article> 한 채를 만들어 돌려줘요.
export function renderPost(post) {
  const article = document.createElement("article");
  article.innerHTML = `
    <header class="post-header">
      <a class="post-user" href="profile.html">
        <img class="post-avatar" src="${post.avatar}" alt="${post.username} 프로필 사진" width="32" height="32">
        <strong class="post-author">${post.username}</strong>
      </a>
      <time class="post-time">${post.time}</time>
    </header>
    <figure>
      <img src="${post.image}" alt="${post.alt}" width="600" height="600" loading="lazy">
    </figure>
    <p class="post-likes">좋아요 <strong>${post.likes.toLocaleString()}</strong>개</p>
    <p class="post-caption"><strong>${post.username}</strong> ${post.caption}</p>
    <ul class="comment-list"></ul>
    <form class="comment-form">
      <textarea class="comment-input" rows="1" placeholder="댓글 달기..." aria-label="댓글 입력"></textarea>
      <button type="submit">게시</button>
    </form>
  `;
  return article;
}

(위는 핵심만 추린 발췌예요. 실제 파일엔 좋아요·댓글·저장 버튼이 담긴 .post-actions도 함께 들어 있어요.)

따라가 볼게요. 먼저 createElement("article")로 빈 게시물 칸을 하나 만들어요. 그다음 그 안을 innerHTML로 채우는데, 여기서 C-5에서 배운 템플릿 리터럴(백틱) 이 진가를 발휘해요. ${post.username}·${post.likes}처럼, 데이터 객체의 값을 마크업 사이사이에 끼워 넣는 거예요.

post.likes.toLocaleString()도 눈여겨보세요. D-1에서 좋아요 숫자를 천 단위 콤마로 보일 때 쓴 그 방법이에요. 1240"1,240"으로 바꿔줘요. 데이터엔 숫자로 저장하고, 화면엔 보기 좋게 콤마를 찍는 거죠.

핵심은 이 함수가 데이터에 따라 다른 게시물을 그린다는 거예요. jaehoon의 데이터를 넣으면 jaehoon의 게시물이, minari_cafe의 데이터를 넣으면 그 게시물이 나와요. 똑같은 틀에 데이터만 바꿔 끼우는 거예요. 게시물이 8개든 800개든, 이 함수 하나면 전부 그릴 수 있어요.

💡 renderPost(post)는 게시물 객체 하나를 받아, D-1에서 배운 createElement+innerHTML<article> 한 채를 만들어 돌려줘요. 템플릿 리터럴(백틱)로 데이터 값을 마크업에 끼워 넣어요. 같은 틀에 데이터만 바꾸면 어떤 게시물이든 그려져요.


Step 7: "고정 화면을 갈아끼우다" — fetch와 render 통합

부품이 다 모였어요. 데이터를 받는 fetchPosts(Step 5)와, 데이터로 게시물을 그리는 renderPost(Step 6)요. 이제 둘을 이어 붙여서, 페이지가 열리면 서버에서 받아 와 화면에 그리게 만들어요.

먼저 feed.html을 손봐야 해요. 지난 시간까지 거기엔 게시물 <article> 10개가 글자로 박혀 있었죠. 이제 그걸 싹 비워요. 게시물은 JavaScript가 그릴 거니까요. 스토리 바는 그대로 두고, 게시물이 들어갈 자리만 주석으로 표시해둬요.

<!-- instagram-clone-frontend/feed.html -->
<div class="feed-main">
  <!-- 스토리 바는 그대로 -->
  <section class="stories" aria-label="스토리"> ... </section>

  <!-- 게시물은 js/feed.js 가 서버에서 받아 여기에 그려요 -->
</div>

이제 feed.js에서 둘을 연결해요. 서버에서 게시물을 받아, 하나씩 renderPost로 그려 .feed-main에 붙여요.

// instagram-clone-frontend/js/feed.js
import { fetchPosts } from "./api.js";

// 페이지가 열리면 서버에서 게시물을 받아 .feed-main 에 차례로 그려요.
async function loadFeed() {
  const posts = await fetchPosts();              // 서버 응답을 기다려요
  const feedMain = document.querySelector(".feed-main");
  for (const post of posts) {
    feedMain.append(renderPost(post));           // 데이터 한 칸 → article 한 채
  }
}

loadFeed();

loadFeed는 먼저 await fetchPosts()로 게시물 배열을 받아요. 그다음 for...of로 게시물을 하나씩 돌면서, 각각을 renderPost로 그려 .feed-mainappend(맨 뒤에 붙이기)해요. 데이터 8칸이면 게시물 8채가 그려지는 거죠. 마지막 loadFeed()로 페이지가 열리자마자 이 과정을 실행해요.

여기서 D-2에서 만든 이벤트 위임이 빛을 발해요. 기억하시죠? 우리는 하트·댓글·삭제 클릭을 버튼마다가 아니라 main 한 곳에서 받았어요. 그런데 지금 게시물은 fetch가 끝난 뒤에야 생기잖아요. 만약 버튼마다 리스너를 달았다면, 아직 존재하지도 않는 버튼엔 리스너를 못 달았을 거예요.

하지만 우리는 부모인 main에 리스너를 달았으니, 나중에 그려진 게시물의 클릭도 그대로 main까지 올라와서 잡혀요. D-2에서 "나중에 생긴 삭제 버튼도 위임이라 잡힌다"고 했던 그 원리가, 오늘 fetch로 그린 게시물 전체에 똑같이 적용되는 거예요. 데이터로 그린 게시물의 하트를 눌러도 좋아요가 켜지고, 댓글도 달려요. 위임 하나 잘 깔아둔 덕을 오늘 제대로 보는 거죠.

💡 loadFeedawait fetchPosts()로 데이터를 받아, for...of로 하나씩 renderPost.feed-main에 붙여요. 핵심은 게시물이 fetch 뒤에 생기는데도, D-2의 이벤트 위임 덕분에 그 게시물의 클릭이 그대로 동작한다는 거예요.


Step 8: "데이터가 안 오면?" — 에러 처리 기초

지금까지는 서버가 항상 잘 응답한다고 가정했어요. 그런데 현실은 안 그래요. 서버가 꺼져 있을 수도, 네트워크가 끊길 수도, 주소를 잘못 적었을 수도 있어요. 이럴 때 아무 대비가 없으면 화면이 깨진 채 멈춰버려요. 그래서 fetch에러 처리가 거의 필수예요.

두 가지를 챙겨야 해요. 첫째, 서버가 응답은 했는데 "그런 데이터 없음" 같은 실패 응답을 줬을 때예요. 서버 응답엔 항상 상태 코드(status) 라는 번호가 붙어 와요.

  200  →  OK (성공)              ← 정상
  404  →  Not Found (없음)       ← 주소가 틀렸거나 데이터가 없음
  500  →  Server Error (서버 탈)  ← 서버 쪽 문제

이 중 200번대(200~299)가 성공이에요. fetch의 응답 객체엔 response.ok라는 값이 있는데, 상태 코드가 200번대면 true, 아니면 false예요. 둘째, 서버가 아예 응답조차 못 하는 경우(서버가 꺼짐·네트워크 끊김)예요. 이땐 fetch 자체가 오류를 던져요. 이 두 가지를 try...catch로 한 번에 감싸요. 우리 api.js의 최종 모습이에요.

// instagram-clone-frontend/js/api.js
export async function fetchPosts() {
  try {
    const response = await fetch(`${BASE_URL}/posts`);
    if (!response.ok) {
      // 상태 코드가 200~299 가 아니면(404·500 등) 실패로 봐요
      throw new Error(`서버 응답 오류: ${response.status}`);
    }
    const posts = await response.json();
    return posts;
  } catch (error) {
    // 서버가 꺼져 있거나 네트워크가 끊기면 여기로 와요
    console.error("게시물을 불러오지 못했어요:", error.message);
    return []; // 빈 배열을 돌려 화면이 깨지지 않게 해요
  }
}

try 블록 안에서 정상 흐름을 적고, 문제가 생기면 catch가 받아요. if (!response.ok)로 실패 응답을 잡아 일부러 오류를 던지면(throw), 그 오류도 catch로 굴러떨어져요. 서버가 아예 꺼져 있어서 fetch가 오류를 던질 때도 마찬가지로 catch가 받고요.

catch에서는 콘솔에 무슨 일이 났는지 찍고, 빈 배열([])을 돌려줘요. 이게 중요해요. 빈 배열을 돌려주면, Step 7의 for...of가 "그릴 게 없네" 하고 그냥 넘어가서 화면이 깨지지 않아요. 게시물은 안 보여도, 적어도 앱이 멈추거나 에러 화면이 뜨진 않는 거죠.

직접 확인해보고 싶다면, json-server를 켠 터미널에서 서버를 꺼보세요(Ctrl+C). 그 상태로 feed.html을 새로고침하면, 게시물은 안 뜨지만 콘솔에 "게시물을 불러오지 못했어요"가 차분히 찍혀요. 화면이 박살 나는 대신, 우리가 정한 대로 얌전히 넘어가는 거예요. 이런 "안 될 때를 대비하는" 습관이 진짜 앱과 장난감 앱을 가르는 차이예요.

💡 서버는 언제든 실패할 수 있어요. 응답 상태 코드가 200번대인지 response.ok로 확인하고, try...catch로 네트워크 오류까지 감싸요. 실패하면 빈 배열을 돌려 화면이 깨지지 않게 해요. 상태 코드(200/404/500)는 다음 시간에 더 자세히 다뤄요.


Step 9: "종합 — 서버를 껐다 켜보며"

오늘 만든 흐름을 한자리에 모아볼게요. 데이터가 화면이 되기까지, 이렇게 흘러가요.

  ① json-server 켜기        →  3001 포트에서 db.json 의 게시물이 대기
  ② feed.html 열기 (Live Server)
  ③ loadFeed() 실행          →  fetchPosts() 로 서버에 GET 요청
  ④ 서버가 JSON 응답          →  게시물 8개 배열 도착
  ⑤ renderPost 로 하나씩 그림 →  .feed-main 에 article 8채 등장
  ⑥ 이벤트 위임(D-2)이 작동   →  그려진 게시물의 하트·댓글도 동작

진짜로 돌려볼 시간이에요. 터미널 두 개가 필요한 거, 잊지 마세요.

# 터미널 1 — 서버 (켜둔 채로)
npx json-server mock/db.json --port 3001

# 터미널 2 — VS Code 에서 feed.html 을 Live Server 로 열기

둘 다 띄우고 feed.html을 보면, 게시물이 화면에 좌르륵 그려져요. 지난 시간과 똑같아 보이지만, 속은 완전히 달라요. 게시물이 HTML에 박혀 있는 게 아니라, 서버에서 받아 와 그려진 거예요.

여기서 작은 실험을 하나 해보세요. db.json을 열어서 게시물 하나의 caption을 아무거나 바꾸고 저장해보세요. 그리고 feed.html을 새로고침하면, 바뀐 내용이 화면에 그대로 반영돼요. 화면 코드(feed.html)는 한 글자도 안 건드렸는데 말이죠. 데이터와 화면이 분리됐다는 게 바로 이 의미예요. 이제 데이터만 바꾸면 화면이 따라와요.

반대로 터미널 1에서 서버를 꺼보세요(Ctrl+C). 그 상태로 새로고침하면 Step 8에서 만든 에러 처리가 작동해서, 화면은 비지만 콘솔에 차분한 메시지가 찍혀요. 서버를 다시 켜고 새로고침하면 게시물이 돌아오고요. "서버가 켜져 있으면 데이터가 흐르고, 꺼지면 끊긴다" — 클라이언트와 서버의 관계를 손으로 직접 느껴보는 거예요.

💡 오늘의 전부는 "데이터는 서버에, 화면은 데이터로"예요. json-server를 켜고 feed.html을 열면, fetch로 받은 데이터가 게시물로 그려져요. db.json만 바꿔도 화면이 따라오고, 서버를 끄면 에러 처리가 받아내요. 꼭 두 터미널을 띄워 직접 껐다 켜보세요.


마무리

오늘 우리는 브라우저 안에 갇혀 있던 데이터를 서버로 끌어냈어요. 화면과 데이터가 한 덩어리였던 정적 클론이, 서버에서 데이터를 받아 그리는 진짜 앱의 모양을 갖추기 시작했죠. 되짚어볼게요.

  • 클라이언트 / 서버 / HTTP — 브라우저(클라이언트)가 요청하면 서버가 응답해요. 손님과 주방의 관계예요.
  • JSON / REST API — 데이터를 주고받는 공통 형식이 JSON. 요청 종류는 4동사(GET/POST/PUT/DELETE)로 나뉘고, 오늘은 GET만 썼어요.
  • json-serverdb.json을 가짜 서버로 띄워줘요. npx json-server mock/db.json --port 3001, 켜둔 채로요.
  • fetch / async-awaitfetch("주소")로 GET 요청, Promise를 async/await로 받아 response.json()으로 풀어요.
  • renderPost — 게시물 객체 하나를 <article> 한 채로 그려요. 같은 틀에 데이터만 갈아끼워요.
  • 에러 처리response.oktry...catch로 실패를 받아내고, 빈 배열로 화면을 지켜요.

한 줄로 외워두세요. 서버에서 fetch로 받고(fetchPosts) → 데이터로 화면을 그리고(renderPost) → 안 될 때를 대비한다(에러 처리). 이 흐름이 앞으로 만들 모든 "서버와 대화하는 화면"의 뼈대예요.

다음 시간 예고

오늘은 데이터를 받아 오기(GET) 만 했어요. 그런데 진짜 인스타라면, 내가 단 댓글이 서버에 저장돼서 새로고침해도 남아 있어야죠. 그러려면 오늘 표로만 봤던 POST(생성)를 직접 써야 해요.

다음 시간(D-4)엔 댓글을 fetch로 서버에 진짜 저장해요. 상태 코드(200/404)도 더 자세히 다루고, 데이터를 보내는 요청은 어떻게 다른지도 배워요. 그리고 스크롤을 내릴 때마다 게시물을 더 불러오는 무한 스크롤, 데이터를 기다리는 동안 보여주는 로딩 표시까지 — 오늘 깔아둔 fetch 위에 진짜 앱다운 기능들을 얹어요. 다음 시간에 만나요!


과제

오늘 배운 fetch 흐름(json-server · fetch · async/await · renderPost · 에러 처리)을 직접 익혀볼 차례예요.

모든 과제는 json-server를 켠 채로(npx json-server mock/db.json --port 3001), feed.html을 Live Server로 열고 화면과 콘솔(F12)에서 결과를 확인하세요. 오늘은 데이터를 받아 오기(GET) 까지만 다뤄요. 저장(POST)은 다음 시간에 해요.

[구현] 데이터를 기다리는 동안 "불러오는 중..." 표시하기

서버 응답엔 시간이 걸려요. 그 사이 화면이 텅 비어 있으면 사용자는 멈춘 줄 알죠. fetch가 끝나기 전까지 "불러오는 중..."을 보여주고, 게시물이 그려지면 지워보세요.

  • .feed-main 안에 로딩 메시지를 담을 요소를 하나 두거나, loadFeed 시작에서 만들어 넣으세요.
  • await fetchPosts()가 끝난 뒤(게시물을 그리기 직전이나 직후)에 그 메시지를 지우세요.
  • 일부러 느리게 보고 싶다면, json-server를 켜기 전에 새로고침해서 "불러오는 중"이 떠 있는 동안을 관찰해보세요.
  • 이건 다음 시간에 배울 "로딩 표시"의 맛보기예요.

[구현] db.json에 내 게시물 추가해서 화면에 띄우기

데이터와 화면이 분리됐다는 걸 직접 체감하는 과제예요. 화면 코드는 한 줄도 안 건드리고, 데이터만 늘려서 게시물을 추가해보세요.

  • mock/db.jsonposts 배열에 새 게시물 객체를 하나 추가하세요(id·username·avatar·time·image·alt·likes·caption·commentCount를 채워서요).
  • json-server를 켠 상태라면, 저장 후 feed.html을 새로고침만 하면 새 게시물이 화면에 나타나요.
  • feed.jsfeed.html전혀 수정하지 않아도 게시물이 늘어난다는 걸 확인하세요. 이게 "데이터로 화면을 그린다"의 핵심이에요.

[탐구] 서버를 끈 채 새로고침하면 무슨 일이 일어날까?

에러 처리가 진짜로 작동하는지 직접 망가뜨려보는 탐구예요.

  • json-server를 켠 터미널에서 Ctrl+C로 서버를 끄세요.
  • 그 상태로 feed.html을 새로고침하고, 콘솔(F12)에 무엇이 찍히는지 관찰하세요.
  • api.jscatch 블록이 어떻게 받아내는지, 그리고 빈 배열([])을 돌려준 덕분에 왜 화면이 "깨지지 않고" 그냥 비는지 정리해보세요.
  • 주소를 일부러 틀리게(/postsX 같은) 바꿔보면 response.ok가 어떻게 반응하는지도 관찰해보세요.

생각해볼 주제

정답을 적는 문제가 아니에요. 오늘 배운 것의 "왜"를 곱씹어보는 질문들이에요. 스스로 답을 만들어본 뒤, 예시답안과 비교해보세요.

1. 게시물을 HTML에 직접 쓰는 것과, 서버에서 받아 그리는 것 — 무엇이 더 나을까?

지난 시간까지 우리 게시물은 feed.html에 글자로 박혀 있었어요. 오늘은 그걸 비우고, 서버에서 받아 renderPost로 그렸죠. 둘 다 결과 화면은 비슷해 보여요.

그런데 게시물이 1만 개라면, 매일 새 게시물이 올라온다면, 사용자마다 다른 게시물을 보여줘야 한다면 어떨까요? HTML에 직접 쓰는 방식은 어디서 한계에 부딪힐까요? 반대로 "데이터와 화면을 분리"하면 무엇이 쉬워지는지, 오늘 db.json만 바꿔도 화면이 따라오던 경험을 떠올리며 생각해보세요.

2. fetch엔 왜 에러 처리가 거의 항상 따라붙을까?

C-4에서 배운 배열의 map이나 D-1의 textContent 같은 코드엔 try...catch를 거의 안 썼어요. 그런데 오늘 fetch엔 에러 처리를 기본으로 깔았죠.

fetch가 하는 일은 다른 코드와 무엇이 다를까요? "내 컴퓨터 안에서 끝나는 일"과 "네트워크 너머 서버에 다녀오는 일"의 차이를 떠올려보세요. 서버가 꺼질 수도, 인터넷이 끊길 수도 있다는 사실이, 왜 fetch를 쓸 때 에러 처리를 "선택이 아니라 기본"으로 만드는지 곱씹어보세요.

✅ 예시 답안정답 보기

여기 있는 답안은 "이것만 정답"이 아니라 모범 사례 중 하나예요. 같은 동작을 만드는 길은 여러 갈래라서, 여러분이 짠 코드가 모양이 조금 달라도 핵심 동작만 맞으면 충분히 잘 푼 거예요. 답안과 비교하면서 "왜 이렇게 했는지"를 곱씹는 게 진짜 공부예요.

모든 과제는 json-server를 켠 채로(npx json-server mock/db.json --port 3001) feed.html을 Live Server로 열고 확인하세요.

과제 1: 데이터를 기다리는 동안 "불러오는 중..." 표시하기

핵심 접근

fetch는 시간이 걸리는 작업이에요. await fetchPosts()가 끝나기 에 로딩 메시지를 보여주고, 데이터가 도착해 게시물을 다 그린 에 그 메시지를 지우면 돼요. 순서가 핵심이에요. "fetch 전 = 표시, fetch 후 = 제거"요.

만들어 넣는 가장 간단한 방법은 loadFeed 맨 앞에서 메시지 요소를 하나 만들어 .feed-main에 넣고, fetch가 끝난 뒤 그 요소를 remove하는 거예요.

예시 구현

// instagram-clone-frontend/js/feed.js
async function loadFeed() {
  const feedMain = document.querySelector(".feed-main");

  // 1) fetch 전 — 로딩 메시지를 만들어 넣어요
  const loading = document.createElement("p");
  loading.textContent = "불러오는 중...";
  loading.className = "feed-loading";
  feedMain.append(loading);

  // 2) 서버 응답을 기다려요 (이 동안 "불러오는 중"이 떠 있어요)
  const posts = await fetchPosts();

  // 3) fetch 후 — 로딩 메시지를 지우고 게시물을 그려요
  loading.remove();
  for (const post of posts) {
    feedMain.append(renderPost(post));
  }
}

await 한 줄이 "여기서 기다린다"는 표시예요. 그 위에 로딩 메시지를 넣어두면, 기다리는 동안 화면에 "불러오는 중..."이 떠 있어요. await가 끝나면(데이터 도착) loading.remove()로 메시지를 지우고 게시물을 그려요. D-1에서 배운 createElement·append·remove를 그대로 쓴 거예요.

json-server를 켜기 전에 feed.html을 먼저 새로고침하면, fetch가 실패할 때까지 "불러오는 중"이 잠깐 떠 있는 걸 볼 수 있어요(서버가 없으면 에러 처리로 빈 배열이 와서 게시물은 안 그려지지만, 로딩 메시지 흐름은 관찰돼요).

채점 포인트

포인트 무엇을 보는가 배점 가중
fetch 전 표시 await fetchPosts() 이전에 로딩 메시지를 넣었는가
fetch 후 제거 await가 끝난 뒤 메시지를 remove했는가
순서의 이해 "기다리는 동안 표시, 도착 후 제거"라는 비동기 흐름을 이해했는가
D-1 도구 재사용 createElement·append·remove를 활용했는가
화면 확인 로딩이 떴다 사라지는 흐름을 직접 관찰했는가

흔한 실수

  • 로딩 메시지를 await 뒤에 넣었다 → 데이터가 이미 온 뒤라 화면에 보일 틈이 없어요. 반드시 await 에 넣어야 기다리는 동안 보여요.
  • 메시지를 지우지 않았다 → 게시물 위에 "불러오는 중..."이 계속 남아 있어요. fetch 후 꼭 remove하세요.
  • feedMain.innerHTML = ""로 통째로 비웠다 → 스토리 바까지 지워질 수 있어요. 로딩 메시지만 콕 집어 remove하는 게 안전해요.

실무 개선 포인트 (심화)

  • 빙글빙글 도는 스피너: 지금은 글자로 "불러오는 중"을 보여줬지만, 실무에선 회전하는 아이콘(스피너)이나 게시물 모양의 회색 뼈대(스켈레톤)를 보여줘요. 다음 시간에 더 다뤄요. 원리는 똑같아요. "fetch 전 표시, 후 제거"요.
  • 너무 빠르면 깜빡임: 서버가 아주 빠르면 로딩이 0.05초만 떴다 사라져서 오히려 깜빡임처럼 보여요. 실무에선 "최소 표시 시간"을 두기도 하는데, 지금은 신경 쓰지 않아도 돼요.

과제 2: db.json에 내 게시물 추가해서 화면에 띄우기

핵심 접근

이 과제의 핵심은 "코드를 안 고친다" 예요. feed.jsfeed.html도 건드리지 않고, db.json의 데이터만 늘려서 게시물이 화면에 나타나는 걸 확인하는 거예요. 데이터와 화면이 분리됐다는 걸 손으로 체감하는 과제죠.

새 게시물 객체를 posts 배열에 추가하되, renderPost가 읽는 속성(username·avatar·time·image·alt·likes·caption·commentCount)을 빠짐없이 채우는 게 포인트예요.

예시 구현

// instagram-clone-frontend/mock/db.json
{
  "posts": [
    { "id": 1, "username": "jaehoon", "...": "기존 게시물들" },

    {
      "id": 9,
      "username": "my_first_post",
      "avatar": "https://picsum.photos/seed/me/64/64",
      "time": "방금",
      "image": "https://picsum.photos/seed/myphoto/600/600",
      "alt": "내가 추가한 첫 게시물 사진",
      "likes": 7,
      "caption": "fetch 로 그려진 내 첫 게시물 #첫게시물",
      "commentCount": 0
    }
  ]
}

기존 게시물 배열 맨 뒤에 객체를 하나 더 붙였어요. id는 기존과 겹치지 않게 9를 줬고요. json-server를 켠 상태라면, 이 파일을 저장하는 순간 서버가 바뀐 데이터를 알아채요. feed.html을 새로고침하면, 새 게시물이 맨 아래 그려져요.

여기서 꼭 확인할 게 있어요. feed.jsfeed.html도 한 글자 안 고쳤다는 거예요. 그런데도 게시물이 늘었죠. renderPost가 데이터를 받아 그리도록 만들어뒀으니, 데이터만 늘리면 화면이 알아서 따라오는 거예요. 이게 "데이터로 화면을 그린다"의 진짜 의미예요.

채점 포인트

포인트 무엇을 보는가 배점 가중
코드 미수정 feed.js·feed.html을 안 고치고 게시물을 늘렸는가
속성 빠짐없이 renderPost가 읽는 속성을 모두 채웠는가 (하나 빠지면 그 자리가 undefined로 나옴)
id 중복 회피 기존과 겹치지 않는 id를 줬는가
데이터-화면 분리 이해 "데이터만 바꿔도 화면이 따라온다"를 설명할 수 있는가
새로고침 확인 저장 후 새로고침으로 반영을 확인했는가

흔한 실수

  • 속성 이름을 다르게 적었다captiontext로 적는 식이면, renderPostpost.caption을 찾는데 없으니 그 자리가 undefined로 떠요. 데이터의 key 이름이 renderPost가 읽는 이름과 정확히 같아야 해요.
  • JSON 문법을 어겼다 → 객체 사이 쉼표를 빠뜨리거나, 맨 마지막 객체 뒤에 쉼표를 남기면 json-server가 파일을 못 읽어요. 저장 후 서버 터미널에 오류가 안 떴는지 확인하세요.
  • feed.js를 고쳐서 게시물을 늘렸다 → 동작은 하지만 이 과제의 목적을 놓친 거예요. 핵심은 "코드 말고 데이터만" 늘리는 경험이에요.

실무 개선 포인트 (심화)

  • 진짜로는 서버에 저장: 지금은 db.json을 손으로 고쳐 게시물을 늘렸지만, 진짜 인스타는 사용자가 글을 올리면 POST 요청으로 서버에 저장돼요. 다음 시간에 그 POST를 배워요. 그러면 손으로 파일을 고치는 대신, 화면에서 글을 써서 데이터를 늘릴 수 있어요.
  • 데이터 검증: 실무 서버는 들어오는 데이터가 올바른지(빈 캡션은 아닌지, 이미지 주소가 맞는지) 검사해요. json-server는 그런 검사 없이 그냥 받아주니, 지금은 우리가 직접 형식을 잘 맞춰야 해요.

과제 3: 서버를 끈 채 새로고침하면 무슨 일이 일어날까?

핵심 접근

이건 코드를 짜는 과제가 아니라, 일부러 망가뜨려보며 관찰하는 탐구예요. 서버를 끄면 fetch가 실패하는데, 우리가 api.js에 깔아둔 try...catch가 그 실패를 어떻게 받아내는지 눈으로 확인하는 거죠.

핵심은 두 가지를 구분하는 거예요. 서버가 아예 꺼진 경우(fetch 자체가 오류를 던짐)와, 서버는 살아 있지만 주소가 틀린 경우(response.okfalse)예요. 둘 다 결국 catch로 모여 빈 배열이 돌아와요.

예시 관찰

api.js의 에러 처리 코드를 다시 보면서 흐름을 따라가 볼게요.

// instagram-clone-frontend/js/api.js
export async function fetchPosts() {
  try {
    const response = await fetch(`${BASE_URL}/posts`);
    if (!response.ok) {
      throw new Error(`서버 응답 오류: ${response.status}`);
    }
    const posts = await response.json();
    return posts;
  } catch (error) {
    console.error("게시물을 불러오지 못했어요:", error.message);
    return [];
  }
}

서버를 껐을 때(Ctrl+C): fetch가 갈 곳이 없으니 곧바로 오류를 던져요. 그러면 try 블록이 중단되고 바로 catch로 굴러떨어져요. 콘솔에 "게시물을 불러오지 못했어요"가 찍히고, 빈 배열이 돌아와요. loadFeedfor...of는 빌 배열이라 한 바퀴도 안 돌고, 화면은 깨지지 않고 그냥 비어요.

주소를 틀렸을 때(/postsX 같은): 이번엔 서버는 살아 있어서 응답은 와요. 하지만 그런 데이터가 없으니 상태 코드 404로 응답해요. response.okfalse가 되고, if (!response.ok)가 일부러 오류를 던져요(throw). 그 오류도 같은 catch로 모여요.

  서버 꺼짐        →  fetch 가 오류 던짐         →  catch
  주소 틀림(404)   →  response.ok = false → throw →  catch
                                                      │
                                          콘솔에 메시지 + 빈 배열 반환
                                          → 화면은 비지만 안 깨짐

두 경로가 결국 한 catch로 모이는 게 핵심이에요. 무슨 이유로 실패하든, 우리는 "콘솔에 알리고, 빈 배열로 화면을 지킨다"는 한 가지 방법으로 대응하는 거예요.

채점 포인트

포인트 무엇을 보는가 배점 가중
서버 꺼짐 관찰 서버를 끄고 새로고침해 콘솔 메시지를 확인했는가
두 실패 경로 구분 "서버 꺼짐(fetch 오류)"과 "주소 틀림(response.ok false)"을 구분했는가
catch로 모임 이해 두 경로가 모두 같은 catch로 처리됨을 설명했는가
빈 배열의 역할 [] 반환 덕분에 화면이 "깨지지 않고 빈다"를 이해했는가
response.ok 관찰 틀린 주소로 response.ok/status가 어떻게 바뀌는지 봤는가

흔한 실수

  • "에러가 나면 무조건 화면이 깨진다"고 생각했다 → 에러를 처리하지 않으면 그렇지만, 우리는 try...catch로 받아냈어요. 처리된 에러는 앱을 멈추지 않아요.
  • response.ok와 fetch 오류를 같은 거로 봤다 → 둘은 달라요. 서버가 응답을 주긴 했는데 실패 응답(404)이면 response.okfalse이고, 서버가 아예 응답을 못 하면 fetch가 오류를 던져요. fetch는 404를 "성공한 통신"으로 봐서 자동으로 오류를 던지지 않거든요. 그래서 response.ok 검사가 따로 필요해요.
  • 빈 배열 대신 null을 돌려줬다loadFeedfor...ofnull을 돌리려다 오히려 오류가 나요. 빈 배열([])이어야 "돌 게 없네" 하고 안전하게 넘어가요.

실무 개선 포인트 (심화)

  • 사용자에게도 알리기: 지금은 실패를 콘솔에만 찍었어요. 진짜 앱이라면 화면에 "게시물을 불러오지 못했어요. 다시 시도"처럼 사용자가 보는 메시지와 재시도 버튼을 보여줘요. 콘솔은 개발자만 보니까요.
  • 상태 코드별 대응: 404(없음)와 500(서버 탈)은 사용자에게 다른 메시지를 보여주는 게 좋아요. 다음 시간에 상태 코드를 더 자세히 다루면, 이런 갈래도 만들 수 있어요.

생각해볼 주제 1: 게시물을 HTML에 직접 쓰는 것과, 서버에서 받아 그리는 것 — 무엇이 더 나을까?

[문제 상황 요약]

지난 시간까지 게시물은 feed.html에 글자로 박혀 있었어요. 오늘은 그걸 비우고, 서버에서 받아 renderPost로 그렸죠. 둘 다 결과 화면은 비슷해 보여요. 그런데 게시물이 1만 개라면, 매일 새 게시물이 올라온다면, 사용자마다 다른 게시물을 봐야 한다면 어떨까요?

[튜터의 가이드 및 해설]

이 질문의 핵심은 "데이터가 변하는가" 예요. 화면에 보일 내용이 고정이면 HTML에 직접 써도 충분하지만, 내용이 계속 바뀐다면 이야기가 달라져요.

두 방식을 비교해볼게요.

  • Option A — HTML에 직접 쓰기: 게시물마다 <article> 마크업을 손으로 적어요. 게시물이 몇 개 안 되고 거의 안 바뀌는 페이지(회사 소개·공지처럼)엔 단순하고 좋아요. 하지만 게시물이 1만 개면 마크업을 1만 번 적어야 하고, 새 게시물이 올라올 때마다 HTML 파일을 직접 고쳐 다시 배포해야 해요. 사용자마다 다른 게시물을 보여주는 건 아예 불가능하고요. 모두가 똑같은 HTML 파일을 받으니까요.

  • Option B — 데이터로 그리기: 게시물 틀(renderPost)은 하나만 만들고, 데이터(db.json)를 받아 그 틀에 끼워 그려요. 게시물이 1만 개면 데이터 1만 칸을 받아 1만 번 그리면 되고, 마크업은 여전히 틀 하나예요. 새 게시물은 데이터만 늘리면 화면이 따라와요(오늘 db.json만 바꿔도 화면이 바뀌던 그 경험이죠). 사용자마다 다른 데이터를 내려주면, 같은 틀로 사람마다 다른 피드를 그릴 수 있고요.

  HTML 직접:   게시물 1만 개 = 마크업 1만 번 손으로 적기, 바뀌면 파일 수정+재배포
  데이터로:    틀 1개 + 데이터 1만 칸,  바뀌면 데이터만 교체 → 화면 자동 반영

현업에서는 보통 내용이 자주 바뀌거나 사용자마다 달라지는 화면(피드·검색 결과·장바구니)은 거의 다 데이터로 그려요. 반대로 거의 안 바뀌는 페이지(서비스 소개·약관)는 HTML에 직접 쓰는 게 더 간단하고요. 즉 "이 화면의 내용이 고정인가, 자주 변하는가"가 두 방식을 가르는 기준이에요. 인스타 피드는 매 순간 새 게시물이 쏟아지니, 당연히 데이터로 그리는 쪽이에요.

🎯 면접관을 홀리는 핵심 멘트

"화면 내용이 고정이면 HTML에 직접 써도 되지만, 자주 바뀌거나 사용자마다 다른 데이터라면 화면 틀과 데이터를 분리해야 합니다. 틀을 한 번만 만들고 데이터를 받아 그리면, 게시물이 1만 개로 늘어도 마크업은 틀 하나뿐이고, 데이터만 교체하면 화면이 따라옵니다. 그래서 피드·검색 결과처럼 동적인 화면은 데이터로 렌더링하고, 소개·약관처럼 고정된 화면은 정적으로 둡니다. 기준은 '내용이 변하는가'입니다."


생각해볼 주제 2: fetch엔 왜 에러 처리가 거의 항상 따라붙을까?

[문제 상황 요약]

C-4의 배열 map이나 D-1의 textContent 같은 코드엔 try...catch를 거의 안 썼어요. 그런데 오늘 fetch엔 에러 처리를 기본으로 깔았죠. fetch가 하는 일은 다른 코드와 무엇이 다르길래, 에러 처리가 "선택이 아니라 기본"이 될까요?

[튜터의 가이드 및 해설]

이 질문의 핵심은 "내 손이 닿는 일인가, 닿지 않는 일인가" 예요. 코드가 하는 일을 두 종류로 나눠보면 답이 보여요.

  • Option A — 내 컴퓨터 안에서 끝나는 일: map으로 배열을 돌리거나, textContent로 글자를 바꾸는 건 전부 내 브라우저 안에서 일어나요. 중간에 "인터넷이 끊겨서 실패"할 일이 없어요. 코드가 맞으면 항상 같은 결과가 나오죠. 그래서 굳이 try...catch로 감쌀 필요가 거의 없어요.

  • Option B — 네트워크 너머로 다녀오는 일: fetch는 내 컴퓨터를 떠나 멀리 있는 서버까지 다녀와야 해요. 그 길에는 내가 어쩔 수 없는 변수가 잔뜩 있어요. 서버가 꺼져 있을 수도, 인터넷이 끊길 수도, 서버가 갑자기 탈이 날 수도 있죠. 내 코드가 아무리 완벽해도, 나 밖의 사정으로 실패할 수 있는 거예요. 그래서 "실패할 수 있다"를 전제로 깔고 try...catch로 대비해두는 거예요.

  map / textContent  →  내 컴퓨터 안에서 끝남   →  실패할 일 거의 없음  →  try-catch 불필요
  fetch              →  네트워크 너머 서버까지   →  서버·네트워크 탓 실패 가능  →  try-catch 기본

비유하면, 내 방에서 책을 꺼내는 일(내 손 안)은 거의 실패하지 않지만, 택배로 물건을 주문하는 일(남의 손을 거침)은 배송이 늦거나 분실될 수 있죠. 그래서 택배엔 "안 오면 어떻게 할지"를 미리 생각해두잖아요. fetch도 똑같이, "응답이 안 오면 어떻게 할지"를 catch에 미리 적어두는 거예요.

현업에서는 보통 "네트워크를 타는 모든 코드는 실패할 수 있다"를 기본 전제로 둬요. fetch뿐 아니라 서버와 대화하는 일은 전부요. 그래서 에러 처리를 빠뜨리면 "행복한 경우만 생각한 코드"라고 지적받아요. 사용자의 인터넷은 언제든 끊길 수 있고, 서버는 언제든 점검에 들어갈 수 있으니까요. "잘 될 때"만이 아니라 "안 될 때"까지 코드에 담는 게, 오늘 우리가 빈 배열로 화면을 지킨 이유예요.

🎯 면접관을 홀리는 핵심 멘트

"map이나 textContent는 내 브라우저 안에서 끝나는 일이라 실패할 변수가 거의 없습니다. 반면 fetch는 네트워크 너머 서버까지 다녀오는 일이라, 서버가 꺼지거나 인터넷이 끊기는 등 제 코드 밖의 이유로 실패할 수 있습니다. 그래서 네트워크를 타는 코드는 '실패할 수 있다'를 전제로 try...catchresponse.ok 검사를 기본으로 둡니다. 행복한 경로만이 아니라 실패 경로까지 처리해, 실패하더라도 앱이 멈추지 않고 빈 화면이나 안내 메시지로 안전하게 넘어가게 만드는 것이 핵심입니다."

더 배우려면

실무 프로젝트까지 가고 싶다면

팀스파르타 백엔드 부트캠프에서 인스타그램 클론을 풀스택으로 완성합니다.