🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

json() 기본 사용법 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 12. 10. · 17 Views

json() 기본 사용법 완벽 가이드

초급 개발자를 위한 json() 메서드 완벽 가이드입니다. Fetch API로 데이터를 받을 때 필수로 알아야 할 json() 메서드의 동작 원리부터 실전 활용법까지 실무 사례와 함께 쉽게 설명합니다.


목차

  1. json()_메서드란_무엇인가
  2. 왜_json()이_비동기인가
  3. 실전_예제_사용자_목록_불러오기
  4. POST_요청에서_json()_사용하기
  5. 자주_하는_실수와_해결법
  6. 실전_활용_팁과_성능_최적화

1. json() 메서드란 무엇인가

어느 날 김개발 씨가 처음으로 API를 연동하는 작업을 맡게 되었습니다. 서버에서 데이터를 받아오는 코드를 작성했는데, 콘솔에 찍힌 결과가 이상했습니다.

데이터가 아니라 "[object Response]"라는 알 수 없는 문자열만 출력되었습니다.

json() 메서드는 Fetch API로 받은 Response 객체를 실제 JavaScript 객체로 변환해주는 메서드입니다. 마치 우편으로 받은 소포를 뜯어서 내용물을 꺼내는 것과 같습니다.

이 메서드를 제대로 사용하지 않으면 서버가 보낸 데이터를 읽을 수 없습니다.

다음 코드를 살펴봅시다.

// 기본적인 json() 사용법
async function fetchUserData() {
  // 서버에 데이터 요청
  const response = await fetch('https://api.example.com/user/1');

  // Response 객체를 JSON으로 파싱
  const data = await response.json();

  // 이제 data는 JavaScript 객체입니다
  console.log(data.name); // '홍길동'
  console.log(data.email); // 'hong@example.com'

  return data;
}

김개발 씨는 입사 2주 차 신입 개발자입니다. 오늘 처음으로 백엔드 API와 연동하는 작업을 맡았습니다.

선배가 알려준 대로 fetch() 함수를 사용해서 코드를 작성했는데, 뭔가 이상합니다. javascript const response = await fetch('https://api.example.com/user/1'); console.log(response); 콘솔을 보니 "[object Response]"라는 이상한 문자열만 나옵니다.

분명히 서버에서는 사용자 정보를 보내줬을 텐데 말이죠. 선배 개발자 박시니어 씨가 다가와 모니터를 봅니다.

"아, 김개발 씨. json() 메서드를 빼먹었네요.

그것만 추가하면 될 겁니다." Response 객체의 정체 그렇다면 이 Response 객체는 정확히 무엇일까요? 쉽게 비유하자면, Response 객체는 마치 포장된 택배 상자와 같습니다.

우편으로 받은 택배 상자에는 송장 정보, 무게, 크기 같은 메타데이터가 붙어 있지만, 실제 물건은 상자 안에 들어 있습니다. 상자를 뜯어야만 내용물을 꺼낼 수 있듯이, Response 객체도 json() 메서드로 열어봐야 실제 데이터를 얻을 수 있습니다.

Response 객체에는 다양한 정보가 담겨 있습니다. 요청이 성공했는지를 나타내는 status 코드, 응답 헤더 정보, 그리고 실제 body 데이터가 그것입니다.

우리가 원하는 데이터는 바로 이 body 안에 들어 있습니다. 왜 json()이 필요한가 fetch()가 반환하는 Response 객체를 그대로 사용할 수 없는 이유가 있습니다.

서버는 데이터를 문자열 형태의 JSON으로 보냅니다. 예를 들어 {"name": "홍길동", "age": 30} 같은 형식입니다.

이것은 우리 눈에는 객체처럼 보이지만, 실제로는 그냥 텍스트 문자열일 뿐입니다. 마치 책에 적힌 글자가 실제 물건이 아니듯이 말이죠.

JavaScript에서 이 문자열을 실제 객체로 사용하려면 파싱(parsing) 과정이 필요합니다. 즉, 문자열 "{"name": "홍길동"}"를 실제 JavaScript 객체 { name: "홍길동" }로 변환해야 합니다.

그래야 data.name 같은 방식으로 속성에 접근할 수 있습니다. json() 메서드의 역할 바로 이런 변환 작업을 해주는 것이 json() 메서드입니다.

json() 메서드는 Response 객체의 body에 담긴 JSON 문자열을 읽어서, 자동으로 JavaScript 객체로 파싱해줍니다. 개발자가 일일이 문자열을 파싱할 필요가 없도록 편리한 기능을 제공하는 것이죠.

중요한 점은 json() 메서드가 비동기로 동작한다는 것입니다. 즉, Promise를 반환하기 때문에 await 키워드를 사용하거나 .then() 메서드로 처리해야 합니다.

코드 분석 위의 코드를 한 줄씩 살펴보겠습니다. 먼저 3번째 줄에서 fetch()를 호출합니다.

이 함수는 서버에 HTTP 요청을 보내고, Response 객체를 담은 Promise를 반환합니다. await 키워드로 기다리면 Response 객체를 받을 수 있습니다.

다음으로 6번째 줄에서 response.json()을 호출합니다. 이 메서드는 Response의 body를 읽어서 JSON 파싱을 시작합니다.

마찬가지로 Promise를 반환하므로 await로 기다려야 합니다. 파싱이 완료되면 JavaScript 객체가 반환됩니다.

이제 9번째 줄부터는 파싱된 data 객체를 자유롭게 사용할 수 있습니다. data.name, data.email 같은 방식으로 속성에 접근하면 됩니다.

실무 활용 사례 실제 프로젝트에서는 어떻게 활용할까요? 예를 들어 쇼핑몰 서비스에서 상품 목록을 불러온다고 가정해봅시다.

백엔드 API는 JSON 형식으로 상품 데이터를 보내줍니다. 프론트엔드에서는 fetch()로 이 데이터를 받아온 뒤, json()으로 파싱해서 화면에 표시합니다.

대부분의 웹 애플리케이션이 이런 패턴을 사용합니다. React 같은 프레임워크에서도 마찬가지입니다.

API로 받아온 데이터를 json()으로 파싱한 뒤, state에 저장해서 컴포넌트에 전달합니다. 주의사항 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 await를 빼먹는 것입니다. const data = response.json()처럼 작성하면 data에는 JavaScript 객체가 아니라 Promise 객체가 담깁니다.

그러면 data.name으로 접근해도 undefined가 나옵니다. 또 다른 실수는 json()을 두 번 호출하는 것입니다.

Response의 body는 한 번만 읽을 수 있습니다. json()을 호출하면 body가 소비되므로, 다시 호출하면 에러가 발생합니다.

정리 다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 코드를 수정했습니다.

await response.json()을 추가하자, 드디어 콘솔에 제대로 된 사용자 정보가 출력되었습니다. json() 메서드는 Fetch API를 사용할 때 반드시 알아야 할 필수 개념입니다.

서버와 통신하는 거의 모든 상황에서 사용하게 될 테니, 확실히 이해하고 넘어가세요.

실전 팁

💡 - json() 메서드는 항상 await 또는 .then()과 함께 사용해야 합니다

  • Response body는 한 번만 읽을 수 있으므로 json()을 중복 호출하지 마세요
  • 서버가 JSON이 아닌 다른 형식(텍스트, 이미지 등)을 보낼 때는 text(), blob() 같은 다른 메서드를 사용하세요

2. 왜 json()이 비동기인가

김개발 씨가 코드를 작성하다가 궁금한 점이 생겼습니다. "왜 json() 메서드는 Promise를 반환하는 걸까요?

그냥 바로 객체를 돌려주면 안 되나요?" 박시니어 씨가 웃으며 대답했습니다. "좋은 질문이네요.

그 이유를 알려드릴게요."

json() 메서드가 비동기로 동작하는 이유는 네트워크로 받은 데이터를 읽는 데 시간이 걸리기 때문입니다. 마치 큰 파일을 다운로드할 때 시간이 걸리는 것처럼, Response body를 읽고 파싱하는 작업도 즉시 완료되지 않습니다.

따라서 Promise를 반환해서 작업이 끝날 때까지 기다릴 수 있게 해줍니다.

다음 코드를 살펴봅시다.

// 비동기 처리 방식 비교
async function example1() {
  const response = await fetch('https://api.example.com/products');

  // 방법 1: async/await 사용
  const data = await response.json();
  console.log(data); // 파싱된 객체
}

async function example2() {
  const response = await fetch('https://api.example.com/products');

  // 방법 2: .then() 사용
  response.json().then(data => {
    console.log(data); // 파싱된 객체
  });
}

김개발 씨는 점점 더 깊은 궁금증이 생기기 시작했습니다. fetch()가 비동기인 건 이해가 됩니다.

네트워크를 통해 서버에 요청을 보내고 응답을 받는 데 시간이 걸리니까요. 하지만 json()은 왜 비동기일까요?

Response 객체를 이미 받았는데 말이죠. 박시니어 씨가 화이트보드에 그림을 그리며 설명하기 시작했습니다.

스트림(Stream)의 개념 Response 객체가 도착했다고 해서 모든 데이터가 한 번에 도착한 것은 아닙니다. 이것을 이해하려면 **스트림(Stream)**이라는 개념을 알아야 합니다.

마치 물이 수도관을 따라 흘러오듯이, 네트워크 데이터도 조금씩 흘러서 도착합니다. 특히 데이터 크기가 크면 여러 조각(chunk)으로 나뉘어서 순차적으로 전송됩니다.

Response 객체가 생성된 시점은 서버가 "응답을 보내기 시작했다"는 것을 의미할 뿐입니다. 실제 body 데이터는 아직 완전히 도착하지 않았을 수 있습니다.

10KB짜리 JSON이라면 금방 도착하겠지만, 10MB짜리 JSON이라면 시간이 꽤 걸립니다. 파싱 작업도 시간이 걸린다 데이터를 전부 받았다고 해도 끝이 아닙니다.

JSON 문자열을 JavaScript 객체로 변환하는 파싱 작업 자체도 연산이 필요합니다. 데이터가 크고 복잡할수록 파싱 시간도 늘어납니다.

예를 들어 수천 개의 상품 정보가 담긴 JSON을 파싱하려면 수십 밀리초 이상 걸릴 수 있습니다. 만약 json() 메서드가 동기로 동작한다면, 파싱이 끝날 때까지 브라우저가 멈춰버립니다.

사용자는 화면을 클릭할 수도, 스크롤할 수도 없게 됩니다. 이것은 최악의 사용자 경험입니다.

비동기로 처리하는 이유 바로 이런 문제를 막기 위해 json() 메서드는 비동기로 설계되었습니다. json()을 호출하면 메서드는 즉시 Promise 객체를 반환합니다.

실제 파싱 작업은 백그라운드에서 진행되고, 브라우저는 다른 작업을 계속할 수 있습니다. 파싱이 완료되면 Promise가 resolve되면서 결과 객체를 전달합니다.

이렇게 하면 큰 데이터를 처리할 때도 UI가 멈추지 않고 부드럽게 동작합니다. 사용자는 데이터가 로딩되는 동안에도 다른 버튼을 클릭하거나 페이지를 스크롤할 수 있습니다.

async/await vs .then() 비동기 작업을 처리하는 방법은 크게 두 가지입니다. 첫 번째는 async/await 문법입니다.

코드가 마치 동기 코드처럼 읽히기 때문에 이해하기 쉽습니다. const data = await response.json()처럼 작성하면, await가 Promise가 완료될 때까지 기다린 뒤 결과를 반환합니다.

두 번째는 .then() 메서드입니다. Promise가 resolve되면 then()에 전달한 콜백 함수가 실행됩니다.

예전 스타일이지만 여전히 많이 쓰입니다. 두 방법 모두 같은 동작을 하지만, async/await가 더 직관적이어서 요즘에는 대부분 이 방식을 선호합니다.

코드 분석 위 코드의 두 가지 방식을 비교해보겠습니다. example1 함수는 async/await를 사용했습니다.

5번째 줄에서 await response.json()이 호출되면, 파싱이 완료될 때까지 이 줄에서 기다립니다. 파싱이 끝나면 data 변수에 결과가 담기고, 다음 줄로 진행됩니다.

example2 함수는 .then()을 사용했습니다. 14번째 줄에서 response.json().then(...)을 호출하면, json() 메서드가 즉시 Promise를 반환하고 함수는 계속 실행됩니다.

파싱이 끝나면 then() 안의 콜백이 나중에 실행됩니다. 실무에서의 선택 실제 프로젝트에서는 어떤 방식을 선호할까요?

대부분의 현대적인 JavaScript 프로젝트는 async/await를 우선적으로 사용합니다. 코드가 순차적으로 읽히기 때문에 이해하고 디버깅하기 쉽습니다.

특히 여러 비동기 작업을 순차적으로 처리할 때 then() 체인보다 훨씬 깔끔합니다. 하지만 여러 작업을 병렬로 처리할 때는 Promise.all()과 함께 사용하는 것이 좋습니다.

이 부분은 나중에 더 자세히 다루겠습니다. 주의사항 비동기 처리를 할 때 주의할 점이 있습니다.

await를 사용하려면 반드시 async 함수 안에서만 사용해야 합니다. 일반 함수에서 await를 쓰면 문법 에러가 발생합니다.

따라서 함수를 정의할 때 async function 키워드를 붙여야 합니다. 또한 .then()을 사용할 때는 에러 처리를 잊지 말아야 합니다.

.catch()를 함께 사용해서 파싱이 실패했을 때를 대비해야 합니다. 정리 박시니어 씨의 설명을 들은 김개발 씨는 이제 이해가 되었습니다.

"아, json()이 비동기인 이유는 데이터를 읽고 파싱하는 데 시간이 걸려서군요. 그동안 브라우저가 다른 일을 할 수 있도록 하는 거네요!" 비동기 프로그래밍은 처음에는 어렵게 느껴질 수 있지만, JavaScript 개발에서 필수적인 개념입니다.

json() 메서드를 통해 비동기의 필요성을 이해했다면, 다른 비동기 작업도 쉽게 다룰 수 있을 것입니다.

실전 팁

💡 - async/await 문법이 .then()보다 직관적이고 읽기 쉽습니다

  • await는 반드시 async 함수 안에서만 사용할 수 있습니다
  • 비동기 작업은 항상 에러 처리를 함께 고려해야 합니다

3. 실전 예제 사용자 목록 불러오기

김개발 씨가 첫 번째 실전 작업을 시작했습니다. 사용자 관리 페이지에서 사용자 목록을 API로 불러와서 화면에 표시하는 기능입니다.

"이제 json()을 제대로 사용해볼 시간이군!" 김개발 씨는 자신감 있게 코드를 작성하기 시작했습니다.

실전 예제에서는 실제 API 엔드포인트에서 사용자 목록을 받아오는 코드를 작성합니다. fetch()로 요청을 보내고, json()으로 파싱한 뒤, 데이터를 화면에 표시하는 전체 흐름을 배웁니다.

에러 처리와 로딩 상태 관리까지 포함한 완전한 예제입니다.

다음 코드를 살펴봅시다.

// 사용자 목록을 불러와서 표시하는 함수
async function loadUsers() {
  try {
    // 로딩 상태 표시
    showLoading();

    // API 요청
    const response = await fetch('https://jsonplaceholder.typicode.com/users');

    // 응답 상태 확인
    if (!response.ok) {
      throw new Error(`HTTP 에러: ${response.status}`);
    }

    // JSON 파싱
    const users = await response.json();

    // 화면에 표시
    displayUsers(users);

  } catch (error) {
    console.error('사용자 목록 로딩 실패:', error);
    showError('사용자 정보를 불러올 수 없습니다.');
  } finally {
    hideLoading();
  }
}

function displayUsers(users) {
  const container = document.getElementById('user-list');
  container.innerHTML = users.map(user => `
    <div class="user-card">
      <h3>${user.name}</h3>
      <p>${user.email}</p>
    </div>
  `).join('');
}

김개발 씨는 이제 실전에 나설 준비가 되었습니다. 회사의 사용자 관리 시스템에서 사용자 목록을 보여주는 기능을 구현해야 합니다.

백엔드 팀에서 이미 API 엔드포인트를 만들어뒀으니, 프론트엔드에서는 이 API를 호출해서 데이터를 받아와 화면에 뿌려주면 됩니다. 간단해 보이지만, 실전에서는 고려해야 할 것들이 많습니다.

전체 흐름 이해하기 먼저 전체적인 흐름을 살펴보겠습니다. 사용자가 "사용자 목록" 메뉴를 클릭하면, 프론트엔드는 백엔드 API에 HTTP GET 요청을 보냅니다.

서버는 데이터베이스에서 사용자 목록을 조회한 뒤 JSON 형식으로 응답합니다. 프론트엔드는 이 JSON을 파싱해서 화면에 예쁘게 표시합니다.

이 과정에서 네트워크 에러가 발생할 수도 있고, 서버가 500 에러를 반환할 수도 있습니다. 데이터를 받아오는 동안에는 로딩 인디케이터를 보여줘야 사용자 경험이 좋아집니다.

try-catch로 에러 처리하기 실전 코드에서 가장 중요한 것은 에러 처리입니다. 네트워크는 언제든지 끊길 수 있고, 서버는 언제든지 에러를 반환할 수 있습니다.

이런 상황을 대비하지 않으면 애플리케이션이 갑자기 멈춰버릴 수 있습니다. 따라서 try-catch 문으로 전체 코드를 감싸는 것이 필수입니다.

try 블록 안에는 정상적인 흐름의 코드를 작성합니다. 만약 어디서든 에러가 발생하면, 즉시 catch 블록으로 점프합니다.

catch 블록에서는 에러를 콘솔에 기록하고, 사용자에게 친절한 메시지를 보여줍니다. response.ok 확인의 중요성 많은 초보 개발자들이 놓치는 부분이 있습니다.

fetch()는 네트워크 요청이 실패해야만 에러를 던집니다. 즉, 서버가 404 Not Found500 Internal Server Error를 반환해도 fetch()는 에러를 던지지 않습니다!

Response 객체를 정상적으로 반환합니다. 따라서 11번째 줄처럼 response.ok 속성을 확인해야 합니다.

이 속성은 HTTP 상태 코드가 200-299 범위일 때만 true입니다. 만약 false라면 수동으로 에러를 던져서 catch 블록으로 보내야 합니다.

이 단계를 빼먹으면, 서버가 에러를 반환했는데도 json()을 호출하게 됩니다. 그러면 파싱 에러가 발생하면서 원인을 알기 어려운 에러 메시지가 나옵니다.

코드 단계별 분석 loadUsers 함수의 각 단계를 살펴보겠습니다. 먼저 5번째 줄에서 showLoading()을 호출해서 로딩 스피너를 표시합니다.

사용자는 "데이터를 불러오는 중이구나"라고 인지할 수 있습니다. 8번째 줄에서 fetch()로 API를 호출합니다.

여기서는 테스트용 API인 JSONPlaceholder를 사용했지만, 실제로는 여러분의 백엔드 API 주소를 넣으면 됩니다. 11번째 줄에서 response.ok를 확인합니다.

서버가 에러를 반환했다면 여기서 에러를 던집니다. 16번째 줄에서 드디어 **response.json()**을 호출합니다.

파싱이 완료되면 users 변수에 사용자 배열이 담깁니다. 19번째 줄에서 displayUsers 함수를 호출해서 화면에 표시합니다.

finally 블록의 역할 25번째 줄의 finally 블록도 중요합니다. finally 블록은 try 블록이 성공하든 실패하든 항상 실행됩니다.

여기서 hideLoading()을 호출해서 로딩 인디케이터를 숨깁니다. 만약 finally를 사용하지 않고 try 블록 끝에 hideLoading()을 넣으면, 에러가 발생했을 때는 실행되지 않아서 로딩 스피너가 계속 돌아가는 문제가 생깁니다.

화면에 데이터 표시하기 29번째 줄부터 시작하는 displayUsers 함수는 파싱된 데이터를 HTML로 변환합니다. users 배열을 map()으로 순회하면서 각 사용자마다 HTML 문자열을 생성합니다.

그 다음 join('')으로 하나의 문자열로 합친 뒤, innerHTML에 할당해서 DOM에 추가합니다. 실제 프로젝트에서는 React나 Vue 같은 프레임워크를 사용하겠지만, 원리는 동일합니다.

파싱된 JavaScript 객체를 컴포넌트에 전달해서 렌더링하는 것이죠. 실무에서의 개선점 이 코드는 기본적인 예제이지만, 실무에서는 더 개선할 부분이 많습니다.

예를 들어 로딩 상태에러 상태를 React의 state로 관리하면 더 체계적입니다. 또한 사용자 목록이 수천 개라면 페이지네이션이나 무한 스크롤을 구현해야 합니다.

성능을 위해 캐싱을 추가할 수도 있습니다. 하지만 기본 원리는 모두 동일합니다.

fetch() → json() → 데이터 표시 → 에러 처리. 이 흐름을 확실히 이해하면 어떤 복잡한 프로젝트에서도 응용할 수 있습니다.

정리 김개발 씨는 코드를 완성하고 테스트해봤습니다. 페이지를 열자 로딩 스피너가 잠깐 나타났다가, 곧 사용자 목록이 화면에 표시되었습니다.

"드디어 제대로 작동하네!" 실전 프로젝트에서 json()을 사용할 때는 에러 처리, 로딩 상태, 응답 상태 확인을 모두 고려해야 합니다. 이 세 가지만 잘 지켜도 안정적인 애플리케이션을 만들 수 있습니다.

실전 팁

💡 - response.ok를 확인해서 HTTP 에러를 감지하세요

  • try-catch-finally로 에러와 로딩 상태를 체계적으로 관리하세요
  • 테스트용 API로는 JSONPlaceholder가 유용합니다

4. POST 요청에서 json() 사용하기

사용자 목록 조회 기능을 완성한 김개발 씨는 이제 새로운 사용자를 추가하는 기능을 구현하게 되었습니다. "GET 요청은 했으니까 POST 요청도 비슷하겠지?" 하지만 POST 요청에는 조금 다른 부분이 있었습니다.

POST 요청에서는 서버에 데이터를 전송할 때도 JSON을 사용하고, 응답을 받을 때도 json()을 사용합니다. 요청 body를 **JSON.stringify()**로 문자열로 변환해서 보내고, 응답은 **response.json()**으로 파싱하는 전체 흐름을 이해해야 합니다.

다음 코드를 살펴봅시다.

// 새로운 사용자를 추가하는 함수
async function createUser(userData) {
  try {
    const response = await fetch('https://jsonplaceholder.typicode.com/users', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      // JavaScript 객체를 JSON 문자열로 변환
      body: JSON.stringify(userData)
    });

    if (!response.ok) {
      throw new Error(`HTTP 에러: ${response.status}`);
    }

    // 서버가 반환한 생성된 사용자 정보를 파싱
    const createdUser = await response.json();

    console.log('사용자 생성 완료:', createdUser);
    return createdUser;

  } catch (error) {
    console.error('사용자 생성 실패:', error);
    throw error;
  }
}

// 사용 예시
const newUser = {
  name: '김개발',
  email: 'kim@example.com',
  phone: '010-1234-5678'
};

createUser(newUser);

김개발 씨는 이제 더 복잡한 작업에 도전합니다. GET 요청은 단순히 데이터를 받아오기만 하면 되지만, POST 요청은 서버에 데이터를 전송해야 합니다.

회원가입 폼에서 사용자가 입력한 정보를 서버에 보내서 새로운 계정을 만드는 것이죠. 박시니어 씨가 설명합니다.

"POST 요청에서는 JSON이 양방향으로 사용됩니다. 보낼 때도 JSON, 받을 때도 JSON이죠." POST 요청의 구조 POST 요청은 GET 요청보다 복잡합니다.

GET 요청은 fetch() 함수에 URL만 전달하면 되지만, POST 요청은 두 번째 인자로 옵션 객체를 전달해야 합니다. 이 객체에는 HTTP 메서드, 헤더, 그리고 전송할 body 데이터가 포함됩니다.

5번째 줄의 method: 'POST'는 이것이 POST 요청임을 명시합니다. 6-8번째 줄의 headers는 요청에 대한 메타데이터를 담고 있습니다.

특히 Content-Type: application/json은 "내가 보내는 데이터는 JSON 형식이야"라고 서버에 알려주는 역할을 합니다. JSON.stringify()의 역할 10번째 줄이 핵심입니다.

body 속성에는 서버로 보낼 데이터를 넣습니다. 하지만 여기서 중요한 점은, 우리가 가진 JavaScript 객체를 그대로 보낼 수 없다는 것입니다.

네트워크를 통해서는 문자열만 전송할 수 있습니다. 따라서 JSON.stringify() 함수를 사용해서 JavaScript 객체를 JSON 문자열로 변환해야 합니다.

예를 들어 { name: "김개발" } 같은 객체는 '{"name":"김개발"}' 같은 문자열로 변환됩니다. 이것은 json() 메서드의 정반대 작업입니다.

json()은 JSON 문자열을 객체로 바꾸고, stringify()는 객체를 JSON 문자열로 바꿉니다. 마치 포장과 개봉의 관계와 같습니다.

서버의 응답 처리 POST 요청을 보내면 서버는 보통 생성된 리소스의 정보를 응답으로 돌려줍니다. 예를 들어 새 사용자를 생성했다면, 서버는 생성된 사용자의 정보(자동으로 부여된 ID 포함)를 JSON으로 반환합니다.

18번째 줄에서 **response.json()**을 호출해서 이 응답을 파싱합니다. 이 부분은 GET 요청과 완전히 동일합니다.

서버가 JSON을 보내면, 우리는 json() 메서드로 파싱하면 됩니다. HTTP 메서드가 GET이든 POST든 상관없이 말이죠.

요청과 응답의 대칭성 전체 흐름을 정리하면 아름다운 대칭 구조가 보입니다. 클라이언트는 객체를 **JSON.stringify()**로 문자열로 변환해서 보냅니다.

서버는 이 문자열을 받아서 객체로 파싱한 뒤 처리합니다. 그리고 응답을 객체에서 JSON 문자열로 변환해서 돌려줍니다.

클라이언트는 **response.json()**으로 이 문자열을 다시 객체로 파싱합니다. 이렇게 JSON은 클라이언트와 서버 사이에서 데이터를 주고받는 공통 언어 역할을 합니다.

클라이언트가 JavaScript로 작성되었든, 서버가 Python으로 작성되었든, 둘 다 JSON을 이해할 수 있으니까요. 코드 단계별 분석 createUser 함수를 자세히 살펴보겠습니다.

4-11번째 줄에서 fetch()를 호출하며 POST 요청을 보냅니다. userData 객체를 JSON.stringify()로 문자열로 변환해서 body에 넣습니다.

13-15번째 줄에서 response.ok를 확인합니다. 서버가 에러를 반환했다면 여기서 에러를 던집니다.

POST 요청에서는 400 Bad Request(잘못된 데이터), 409 Conflict(중복된 데이터) 같은 에러가 자주 발생합니다. 18번째 줄에서 response.json()으로 서버의 응답을 파싱합니다.

보통 서버는 방금 생성된 리소스의 전체 정보를 반환합니다. 사용 예시 30-35번째 줄은 실제 사용 예시입니다.

먼저 newUser 객체를 만듭니다. 이것은 폼에서 사용자가 입력한 데이터라고 생각하면 됩니다.

그 다음 createUser() 함수에 이 객체를 전달합니다. 함수 내부에서는 이 객체를 JSON 문자열로 변환해서 서버에 전송하고, 서버의 응답을 받아서 파싱한 뒤 반환합니다.

호출하는 쪽에서는 복잡한 JSON 처리를 신경 쓸 필요가 없습니다. 실무 팁 실제 프로젝트에서는 몇 가지 추가 고려사항이 있습니다.

먼저 인증 토큰을 헤더에 포함해야 하는 경우가 많습니다. Authorization: Bearer {token} 같은 형식으로 헤더에 추가하면 됩니다.

또한 폼 데이터를 검증하는 로직이 필요합니다. 사용자가 이메일 형식을 잘못 입력했거나 필수 항목을 빠뜨렸다면, 서버에 요청을 보내기 전에 클라이언트에서 먼저 검사하는 것이 좋습니다.

다른 HTTP 메서드 POST 외에도 PUT, PATCH, DELETE 같은 메서드들도 동일한 방식으로 사용합니다. PUT은 리소스를 완전히 교체할 때, PATCH는 일부만 수정할 때, DELETE는 삭제할 때 사용합니다.

모두 같은 방식으로 fetch()의 method 옵션에 명시하면 되고, 필요하면 body에 데이터를 담아 보내면 됩니다. 정리 김개발 씨는 코드를 작성하고 테스트해봤습니다.

폼에서 데이터를 입력하고 "등록" 버튼을 클릭하자, 서버에서 "생성 완료" 응답이 돌아왔습니다. 콘솔을 보니 자동으로 부여된 ID가 포함된 사용자 객체가 출력되었습니다.

POST 요청에서는 보낼 때 JSON.stringify(), **받을 때 response.json()**을 사용한다는 점만 기억하면 됩니다. 이 패턴은 모든 HTTP 메서드에서 동일하게 적용됩니다.

실전 팁

💡 - POST 요청에서는 Content-Type 헤더를 'application/json'으로 설정하세요

  • body에 넣을 데이터는 JSON.stringify()로 문자열로 변환하세요
  • 서버 응답도 json()으로 파싱하는 것은 GET 요청과 동일합니다

5. 자주 하는 실수와 해결법

김개발 씨는 며칠간 열심히 코딩했지만, 계속해서 같은 에러에 부딪혔습니다. "Unexpected token < in JSON at position 0" 같은 알 수 없는 에러들이 콘솔에 나타났습니다.

박시니어 씨가 다가와 말했습니다. "json()을 사용할 때 흔히 하는 실수들이 있어요.

하나씩 짚어볼까요?"

json() 메서드를 사용할 때 자주 발생하는 실수들을 알아봅니다. await를 빼먹는 실수, 서버가 JSON이 아닌 데이터를 반환하는 경우, json()을 두 번 호출하는 실수, 에러 응답을 파싱하려는 실수 등을 다루고 각각의 해결법을 제시합니다.

다음 코드를 살펴봅시다.

// 실수 1: await 빼먹기 (잘못된 코드)
async function mistake1() {
  const response = await fetch('https://api.example.com/data');
  const data = response.json(); // await 누락!
  console.log(data); // Promise 객체가 출력됨
}

// 실수 2: json()을 두 번 호출 (잘못된 코드)
async function mistake2() {
  const response = await fetch('https://api.example.com/data');
  const data1 = await response.json(); // 첫 번째 호출
  const data2 = await response.json(); // 에러 발생!
}

// 실수 3: 에러 응답도 json()으로 파싱 시도 (잘못된 코드)
async function mistake3() {
  const response = await fetch('https://api.example.com/data');
  const data = await response.json(); // 응답이 HTML이면 파싱 에러!
}

// 올바른 코드: 에러 처리 포함
async function correctWay() {
  try {
    const response = await fetch('https://api.example.com/data');

    // 1. 응답 상태 먼저 확인
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }

    // 2. Content-Type 확인 (선택사항)
    const contentType = response.headers.get('content-type');
    if (!contentType || !contentType.includes('application/json')) {
      throw new Error('응답이 JSON이 아닙니다');
    }

    // 3. json() 파싱
    const data = await response.json();
    return data;

  } catch (error) {
    console.error('에러 발생:', error);
    throw error;
  }
}

김개발 씨는 지난 며칠간 이상한 에러들과 씨름했습니다. 코드는 분명 맞는 것 같은데 계속 에러가 발생합니다.

박시니어 씨가 코드를 보더니 고개를 끄덕였습니다. "아, 이 실수들은 누구나 한 번씩 겪어요." 초보 개발자들이 json()을 사용할 때 자주 하는 실수들이 있습니다.

이 실수들을 미리 알아두면 많은 시간을 절약할 수 있습니다. 실수 1: await를 빼먹는 경우 가장 흔한 실수는 await 키워드를 빼먹는 것입니다.

4번째 줄을 보면 const data = response.json()처럼 작성되어 있습니다. json() 메서드는 Promise를 반환하는데, await를 붙이지 않으면 Promise 객체 자체가 data 변수에 담깁니다.

그러면 5번째 줄에서 data를 출력했을 때 실제 데이터가 아니라 "Promise { <pending> }" 같은 이상한 객체가 나옵니다. data.name으로 속성에 접근하면 undefined가 반환됩니다.

이 문제는 await를 추가하면 간단히 해결됩니다. const data = await response.json()처럼 작성하면 Promise가 완료될 때까지 기다린 뒤 결과값을 받습니다.

실수 2: json()을 두 번 호출하는 경우 두 번째 흔한 실수는 json()을 여러 번 호출하는 것입니다. Response의 body는 스트림이기 때문에 한 번만 읽을 수 있습니다.

11번째 줄에서 첫 번째 json()을 호출하면 body가 소비됩니다. 12번째 줄에서 다시 json()을 호출하면 "body stream already read" 같은 에러가 발생합니다.

만약 응답 데이터를 여러 곳에서 사용해야 한다면, 한 번만 파싱한 뒤 그 결과를 변수에 저장해서 재사용해야 합니다. 또는 response.clone()으로 Response 객체를 복제할 수도 있지만, 일반적으로는 권장하지 않습니다.

실수 3: 서버가 JSON이 아닌 데이터를 반환하는 경우 세 번째 실수는 응답 형식을 확인하지 않는 것입니다. 서버가 항상 JSON을 반환한다고 가정하면 안 됩니다.

특히 에러가 발생했을 때 서버는 HTML 에러 페이지를 반환할 수 있습니다. 18번째 줄에서 이런 HTML 문자열을 json()으로 파싱하려고 하면 "Unexpected token < in JSON" 같은 에러가 발생합니다.

이것은 JSON 파서가 HTML 태그의 < 문자를 만나서 당황한 것입니다. JSON은 {[로 시작해야 하는데 <가 나왔으니까요.

올바른 에러 처리 방법 22-46번째 줄의 correctWay 함수는 올바른 패턴을 보여줍니다. 먼저 27-29번째 줄에서 response.ok를 확인합니다.

서버가 에러 상태 코드를 반환했다면 json()을 호출하기 전에 에러를 던집니다. 이렇게 하면 HTML 에러 페이지를 파싱하려는 시도를 막을 수 있습니다.

다음으로 32-35번째 줄에서 Content-Type 헤더를 확인합니다. 이 단계는 선택사항이지만, 더 안전한 코드를 작성하고 싶다면 추가하는 것이 좋습니다.

서버가 Content-Type: application/json을 보내지 않았다면 JSON이 아닐 가능성이 높습니다. 마지막으로 38번째 줄에서 json()을 호출합니다.

여기까지 왔다면 응답은 정상적인 JSON일 확률이 높습니다. 파싱 에러 처리 그래도 파싱에 실패할 수 있습니다.

서버가 Content-Type은 application/json으로 설정했지만 실제로는 잘못된 JSON을 보내는 경우도 있습니다. 이럴 때는 json() 메서드 자체에서 SyntaxError를 던집니다.

다행히 우리는 전체 코드를 try-catch로 감쌌기 때문에, 어디서 에러가 발생하든 catch 블록에서 잡을 수 있습니다. catch 블록에서 에러를 로깅하고 사용자에게 적절한 메시지를 보여주면 됩니다.

네트워크 에러 vs 파싱 에러 에러의 종류를 구분하는 것도 중요합니다. 네트워크 에러는 인터넷 연결이 끊겼을 때 발생합니다.

이때는 fetch() 자체가 에러를 던집니다. HTTP 에러는 서버가 4xx, 5xx 상태 코드를 반환했을 때이고, response.ok로 감지할 수 있습니다.

파싱 에러는 json()이 잘못된 JSON을 만났을 때 발생합니다. 각 에러에 맞는 메시지를 사용자에게 보여주면 더 좋은 사용자 경험을 제공할 수 있습니다.

예를 들어 네트워크 에러면 "인터넷 연결을 확인해주세요", HTTP 500 에러면 "서버에 문제가 발생했습니다" 같은 메시지를 보여주는 식입니다. 디버깅 팁 에러가 발생했을 때 디버깅하는 방법도 알아두면 좋습니다.

먼저 브라우저 개발자 도구의 Network 탭을 열어서 실제 응답 내용을 확인하세요. 서버가 정말 JSON을 보냈는지, 아니면 HTML이나 다른 형식을 보냈는지 눈으로 직접 확인할 수 있습니다.

또한 json()을 호출하기 전에 **response.text()**를 호출해서 원본 문자열을 콘솔에 출력해볼 수도 있습니다. 단, text()와 json()을 둘 다 호출할 수는 없으므로, 디버깅 시에만 text()를 사용하세요.

정리 박시니어 씨의 설명을 들은 김개발 씨는 자신의 코드를 다시 살펴봤습니다. "아, 여기서 await를 빼먹었네요.

그리고 response.ok 체크도 안 했고요." json()을 사용할 때는 await 잊지 않기, response.ok 확인하기, json()은 한 번만 호출하기, 이 세 가지만 기억하면 대부분의 실수를 피할 수 있습니다.

실전 팁

💡 - await를 빼먹지 않도록 주의하세요. Promise 객체를 받으면 실제 데이터를 사용할 수 없습니다

  • json()은 body를 소비하므로 한 번만 호출하세요
  • response.ok를 먼저 확인해서 에러 응답을 json()으로 파싱하지 마세요

6. 실전 활용 팁과 성능 최적화

김개발 씨는 이제 json()을 능숙하게 사용할 수 있게 되었습니다. 하지만 프로젝트가 커지면서 새로운 고민이 생겼습니다.

"여러 API를 동시에 호출해야 할 때는 어떻게 하지? 같은 데이터를 반복해서 가져오는 것도 비효율적인 것 같은데..." 박시니어 씨가 웃으며 말했습니다.

"이제 실전 팁을 배울 시간이네요."

실전에서 json()을 효율적으로 사용하는 방법을 배웁니다. 여러 API를 병렬로 호출하기, 타임아웃 설정하기, 응답 캐싱하기, TypeScript로 타입 안전성 확보하기 등 실무에서 바로 써먹을 수 있는 고급 테크닉들을 다룹니다.

다음 코드를 살펴봅시다.

// 팁 1: 여러 API를 병렬로 호출하기
async function fetchMultipleData() {
  try {
    // Promise.all로 동시에 요청
    const [users, posts, comments] = await Promise.all([
      fetch('https://api.example.com/users').then(r => r.json()),
      fetch('https://api.example.com/posts').then(r => r.json()),
      fetch('https://api.example.com/comments').then(r => r.json())
    ]);

    return { users, posts, comments };
  } catch (error) {
    console.error('데이터 로딩 실패:', error);
    throw error;
  }
}

// 팁 2: 타임아웃 설정하기
async function fetchWithTimeout(url, timeout = 5000) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeout);

  try {
    const response = await fetch(url, {
      signal: controller.signal
    });
    clearTimeout(timeoutId);

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }

    return await response.json();
  } catch (error) {
    if (error.name === 'AbortError') {
      throw new Error('요청 시간 초과');
    }
    throw error;
  }
}

// 팁 3: 간단한 캐싱 구현
const cache = new Map();

async function fetchWithCache(url, maxAge = 60000) {
  const cached = cache.get(url);

  // 캐시가 있고 유효하면 반환
  if (cached && Date.now() - cached.timestamp < maxAge) {
    console.log('캐시에서 반환:', url);
    return cached.data;
  }

  // 새로운 요청
  const response = await fetch(url);
  const data = await response.json();

  // 캐시에 저장
  cache.set(url, {
    data: data,
    timestamp: Date.now()
  });

  return data;
}

김개발 씨는 기본적인 json() 사용법은 완벽히 익혔습니다. 이제는 더 효율적이고 안정적인 코드를 작성하고 싶습니다.

박시니어 씨가 실전에서 자주 쓰이는 패턴들을 하나씩 알려주기 시작했습니다. 병렬 요청으로 성능 개선하기 실무에서는 여러 API를 동시에 호출해야 하는 경우가 많습니다.

예를 들어 대시보드 페이지를 렌더링하려면 사용자 정보, 통계 데이터, 최근 활동 내역 등을 모두 가져와야 합니다. 만약 이 요청들을 순차적으로 처리하면 어떻게 될까요?

첫 번째 요청이 완료될 때까지 기다렸다가 두 번째 요청을 보내고, 또 기다렸다가 세 번째 요청을 보냅니다. 각 요청이 1초씩 걸린다면 총 3초가 소요됩니다.

이것은 너무 느립니다. Promise.all의 마법 2-8번째 줄의 코드는 Promise.all을 사용해서 여러 요청을 병렬로 처리합니다.

Promise.all은 배열로 전달된 모든 Promise를 동시에 실행하고, 모두 완료될 때까지 기다립니다. 세 개의 fetch()가 거의 동시에 시작되므로, 가장 느린 요청의 시간만큼만 기다리면 됩니다.

모두 1초씩 걸린다면 총 1초만 소요되는 것이죠. 5-7번째 줄을 보면 각 fetch() 뒤에 .then(r => r.json())을 체인으로 연결했습니다.

이것은 await fetch().then(r => r.json())을 간결하게 쓴 방식입니다. Promise.all이 모든 json() 파싱이 완료될 때까지 기다린 뒤, 배열 구조 분해를 통해 각 결과를 변수에 담습니다.

타임아웃 설정하기 네트워크 요청은 언제 완료될지 알 수 없습니다. 서버가 응답이 느리거나, 네트워크 상태가 나쁘면 요청이 수십 초씩 걸릴 수 있습니다.

최악의 경우 아예 응답이 오지 않아서 사용자가 무한정 기다려야 할 수도 있습니다. 이런 상황을 방지하려면 타임아웃을 설정해야 합니다.

일정 시간 내에 응답이 오지 않으면 요청을 취소하고 에러를 표시하는 것이죠. AbortController 활용하기 18-39번째 줄의 fetchWithTimeout 함수는 타임아웃을 구현합니다.

19번째 줄에서 AbortController를 생성합니다. 이것은 fetch 요청을 중단할 수 있는 컨트롤러입니다.

20번째 줄에서 setTimeout()으로 일정 시간 후에 controller.abort()를 호출하도록 예약합니다. 23-25번째 줄에서 fetch()를 호출할 때 signal: controller.signal 옵션을 전달합니다.

이렇게 하면 abort()가 호출되었을 때 fetch 요청이 취소됩니다. 26번째 줄에서 clearTimeout()으로 타이머를 제거합니다.

응답이 제시간에 도착했다면 abort()를 호출할 필요가 없으니까요. 34-36번째 줄에서 AbortError를 감지해서 "요청 시간 초과" 같은 친절한 메시지로 변환합니다.

캐싱으로 불필요한 요청 줄이기 같은 데이터를 반복해서 요청하는 것도 비효율적입니다. 예를 들어 사용자 프로필 정보는 자주 바뀌지 않습니다.

페이지를 이동할 때마다 매번 API를 호출하는 것보다, 한 번 받아온 데이터를 메모리에 저장해뒀다가 재사용하는 것이 효율적입니다. 간단한 캐시 구현 42-64번째 줄의 fetchWithCache 함수는 간단한 캐싱을 구현합니다.

43번째 줄에서 Map 객체를 캐시 저장소로 사용합니다. Map은 키-값 쌍을 저장하는 자료구조로, 여기서는 URL을 키로, 데이터와 타임스탬프를 값으로 저장합니다.

46-50번째 줄에서 캐시를 확인합니다. 해당 URL의 캐시가 있고, 아직 유효 기간(maxAge) 내라면 캐시된 데이터를 즉시 반환합니다.

네트워크 요청을 하지 않으므로 매우 빠릅니다. 53-54번째 줄에서 캐시가 없거나 만료되었다면 실제 fetch()를 호출하고 json()으로 파싱합니다.

57-60번째 줄에서 새로 받아온 데이터를 캐시에 저장합니다. 데이터와 함께 현재 시각(timestamp)도 저장해서 나중에 유효 기간을 체크할 수 있게 합니다.

캐싱 전략 실무에서는 더 정교한 캐싱 전략이 필요합니다. 시간 기반 캐싱은 위 예제처럼 일정 시간 후 캐시를 무효화합니다.

버전 기반 캐싱은 데이터가 변경되면 캐시를 즉시 무효화합니다. LRU(Least Recently Used) 캐싱은 메모리가 부족하면 가장 오래 사용하지 않은 항목을 제거합니다.

대규모 프로젝트에서는 React Query, SWR 같은 전문 라이브러리를 사용하는 것이 좋습니다. 이 라이브러리들은 캐싱, 리페칭, 낙관적 업데이트 등 고급 기능을 제공합니다.

TypeScript로 타입 안전성 확보하기 TypeScript를 사용한다면 json()의 반환값에 타입을 지정할 수 있습니다. typescript interface User { id: number; name: string; email: string; } async function fetchUser(id: number): Promise<User> { const response = await fetch(`/api/users/${id}`); const data: User = await response.json(); return data; } 이렇게 하면 data 변수를 사용할 때 자동완성이 지원되고, 잘못된 속성에 접근하면 컴파일 에러가 발생합니다.

정리 박시니어 씨의 설명을 들은 김개발 씨는 눈이 반짝였습니다. "와, 이런 테크닉들이 있었군요!

병렬 요청으로 속도를 3배 개선할 수 있다니 놀랍네요." json()의 기본 사용법을 익혔다면, 이제 병렬 처리, 타임아웃, 캐싱 같은 고급 패턴들을 적용해서 더 빠르고 안정적인 애플리케이션을 만들어보세요. 사용자 경험이 크게 향상될 것입니다.

실전 팁

💡 - Promise.all로 여러 API를 병렬로 호출하면 속도가 크게 개선됩니다

  • AbortController로 타임아웃을 설정해서 무한 대기를 방지하세요
  • 자주 사용하는 데이터는 캐싱해서 불필요한 네트워크 요청을 줄이세요
  • React Query, SWR 같은 라이브러리를 사용하면 더 강력한 데이터 관리가 가능합니다

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#JavaScript#json#FetchAPI#AsyncAwait#ResponseParsing#React,State Management,CLI

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.