Original article: JSON Stringify Example – How to Parse a JSON Object with JS

JSON(JavaScript Object Notation의 약자)은 어디를 가든 흔히 접할 수 있는 파일 형식입니다. 웹 애플리케이션을 사용해 본 적이 있다면 JSON을 사용해 서버와 기기 간에 데이터를 구축, 저장 및 전송과 같은 작업을 해봤을 가능성이 높습니다.

이 기사에서는 JSON과 JavaScript의 차이점을 간략하게 설명한 다음 브라우저와 Node.js 프로젝트에서 JavaScript를 사용해 JSON을 구문분석(parsing)하는 다양한 방법을 살펴보겠습니다.

JSON과 JavaScript의 차이

JSON은 일반 JavaScript처럼 보이지만 JSON을 텍스트 파일과 유사한 데이터 형식으로 이해하는 것이 좋습니다. JSON이 JavaScript와 매우 비슷해 보이는 이유는 JSON은 JavaScript 문법에서 영감을 받아 만들어졌기 때문입니다.

JSON 객체와 JSON 배열의 예시를 본 후 JavaScript 객체와 비교해보겠습니다.

JSON 객체와 JavaScript 객체 리터럴(object literal)

우선 JSON 객체 문법을 보여드리겠습니다.

{
  "name": "Jane Doe",
  "favorite-game": "Stardew Valley", 
  "subscriber": false
}
jane-profile.json

JSON 객체와 일반 JavaScript 객체(객체 리터럴이라고도 합니다)의 주요 차이점은 따옴표입니다. JSON 객체의 모든 키(key)와 문자열 유형 값은 큰따옴표(")로 감싸야 합니다.

반면 JavaScript 객체 리터럴은 조금 더 유연합니다. 객체 리터럴을 사용하면 키와 문자열을 큰따옴표로 묶을 필요가 없습니다. 키에 작은따옴표(')를 사용하거나 따옴표 자체를 생략해도 됩니다.

위에 코드를 JavaScript 객체 리터럴 문법으로 변환해본다면 다음과 같습니다.

const profile = {
  name: 'Jane Doe',
  'favorite-game': 'Stardew Valley',
  subscriber: false
}

위의 예시에서는 'favorite-game' 키가 작은따옴표로 묶인 것을 확인할 수 있습니다. 객체 리터럴 문법에서는 대시(-)로 구분된 키는 따옴표로 감싸야 하기 때문입니다.

따옴표를 사용하지 않으려면 키를 카멜 표기법(camel case)인 favoriteGames 또는 언더바 표기법(underscore case)인 favorite_game으로 수정하면 됩니다.

JSON 배열과 JavaScript 배열

JSON 배열은 JavaScript 배열과 거의 같은 방식으로 작동하며 문자열, 불린(boolean), 숫자 및 기타 JSON 개체를 포함할 수 있습니다.

예시:

[
  {
    "name": "Jane Doe",
    "favorite-game": "Stardew Valley",
    "subscriber": false
  },
  {
    "name": "John Doe",
    "favorite-game": "Dragon Quest XI",
    "subscriber": true
  }
]
profiles.json

JavaScript 문법에서는 다음과 같이 표시됩니다.

const profiles = [
  {
    name: 'Jane Doe',
    'favorite-game': 'Stardew Valley',
    subscriber: false
  },
  {
    name: 'John Doe',
    'favorite-game': 'Dragon Quest XI',
    subscriber: true
  }
];

JSON은 사실 문자열

JSON 또한 객체와 배열이 있다면 일반 JavaScript 객체 리터럴이나 배열처럼 프로그램에서 사용할 수 없는지 의문이 들 수도 있습니다.

하지 못하는 이유는 JSON은 사실 문자열에 불과하기 때문입니다.

예를 들어 jane-profile.json 또는 profiles.json과 같은 별도의 파일에 JSON을 작성하면 해당 파일은 JavaScript처럼 보이지만 사실 JSON 객체 또는 JSON 배열 형식의 문자열이 담깁니다.

이때 API에 요청하면 다음과 같은 내용이 반환됩니다.

{"name":"Jane Doe","favorite-game":"Stardew Valley","subscriber":false}

텍스트 파일과 마찬가지로 프로젝트에서 JSON을 사용하려면 해당 파일을 구문분석하거나 프로그래밍 언어가 이해할 수 있는 형식으로 변경해야 합니다. 예를 들어, Python에서 JSON 객체를 구문분석하면 딕셔너리(dictionary)가 생성됩니다.

이러한 이해를 바탕으로 JavaScript에서 JSON을 구문분석하는 다양한 방법을 배워볼까요?

브라우저에서 JSON 구문분석하기

일반적으로 API를 통해 데이터를 수신하거나 보낼 때 브라우저에서 JSON으로 작업하는 경우가 많습니다.

몇 가지 예를 살펴보겠습니다.

fetch를 사용해 JSON 구문분석하기

API에서 데이터를 가져오는 가장 쉬운 방법은 .json() 메서드를 통해 JSON 응답을 JavaScript 객체 리터럴 또는 배열로 자동으로 구문분석하는 fetch를 사용하는 것입니다.

다음 예시는 무료 Chuck Norris Jokes API에서 개발자 관련 유머 데이터에 대한 GET 요청을 하기 위해 fetch를 사용하는 코드입니다.

fetch('https://api.chucknorris.io/jokes/random?category=dev')
  .then(res => res.json()) // .json() 메서드는 JSON 응답을 JavaScript 객체 리터럴로 구문분석합니다.
  .then(data => console.log(data));

브라우저에 해당 코드를 실행하면 콘솔에 다음과 같은 내용이 표시됩니다.

{
    "categories": ["dev"],
    "created_at": "2020-01-05 13:42:19.324003",
    "icon_url": "https://assets.chucknorris.host/img/avatar/chuck-norris.png",
    "id": "elgv2wkvt8ioag6xywykbq",
    "updated_at": "2020-01-05 13:42:19.324003",
    "url": "https://api.chucknorris.io/jokes/elgv2wkvt8ioag6xywykbq",
    "value": "Chuck Norris's keyboard doesn't have a Ctrl key because nothing controls Chuck Norris."
}

해당 내용은 JSON 객체처럼 보이지만 실제로는 JavaScript 객체 리터럴이기 때문에 프로그램에서 자유롭게 사용할 수 있습니다.

JSON.stringify()로 JSON 문자열로 변환하기

API로 데이터를 보내고 싶다면 어떻게 해야 할까요?

예를 들어 Chuck Norris Jokes API에 농담 데이터를 보내 다른 사람들이 나중에 읽을 수 있도록 하고 싶다면 말이죠.

먼저 보내고 싶은 농담 데이터를 JavaScript 객체 리터럴로 작성합니다.

const newJoke = {
  categories: ['dev'],
  value: "Chuck Norris's keyboard is made up entirely of Cmd keys because Chuck Norris is always in command."
};

그런 다음, API로 데이터를 보내기 위해서는 방금 생성한 농담 객체 리터럴을 JSON 문자열로 변환해야 합니다.

JavaScript에는 이런 작업을 실행하기 위한 매우 유용한 메서드가 포함되어 있습니다 - 바로 JSON.stringify()입니다.

const newJoke = {
  categories: ['dev'],
  value: "Chuck Norris's keyboard is made up entirely of Cmd keys because Chuck Norris is always in command."
};

console.log(JSON.stringify(newJoke)); // {"categories":["dev"],"value":"Chuck Norris's keyboard is made up entirely of Cmd keys because Chuck Norris is always in command."}

console.log(typeof JSON.stringify(newJoke)); // string

이 예시에서 객체 리터럴을 JSON 문자열로 변환하는 것처럼 JSON.stringify()는 배열에서도 똑같이 작동합니다.

마지막으로 JSON 문자열로 변환된 농담 데이터를 POST 요청과 함께 API로 다시 보내면 됩니다.

Chuck Norris Jokes API에는 실제로 이 기능이 없지만, 있다고 가정하면 코드는 다음과 같을 것입니다.

const newJoke = {
  categories: ['dev'],
  value: "Chuck Norris's keyboard is made up entirely of Cmd keys because Chuck Norris is always in command."
};

fetch('https://api.chucknorris.io/jokes/submit', { // fake API endpoint
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(newJoke), // JavaScript 객체 리터럴을 JSON 문자열로 변환하기
})
  .then(res => res.json())
  .then(data => console.log(data))
  .catch(err => {
    console.error(err);
  });

이렇게 가져온 JSON을 fetch로 구문분석하고 JSON.stringify()를 사용해 JavaScript 객체 리터럴을 JSON 문자열로 변환했습니다.

브라우저에서 로컬 JSON 파일 작업하기

안타깝게도 브라우저에서 로컬 JSON 파일을 로딩하는 것은 가능하지 않고 권장하지 않습니다.

로컬 파일을 로딩하려고 하면 fetch에서 오류가 발생합니다. 예를 들어 농담 데이터가 포함된 JSON 파일이 있다고 가정해봅시다.

[
  {
    "categories": ["dev"],
    "created_at": "2020-01-05 13:42:19.324003",
    "icon_url": "https://assets.chucknorris.host/img/avatar/chuck-norris.png",
    "id": "elgv2wkvt8ioag6xywykbq",
    "updated_at": "2020-01-05 13:42:19.324003",
    "url": "https://api.chucknorris.io/jokes/elgv2wkvt8ioag6xywykbq",
    "value": "Chuck Norris's keyboard doesn't have a Ctrl key because nothing controls Chuck Norris."
  },
  {
    "categories": ["dev"],
    "created_at": "2020-01-05 13:42:19.324003",
    "icon_url": "https://assets.chucknorris.host/img/avatar/chuck-norris.png",
    "id": "ae-78cogr-cb6x9hluwqtw",
    "updated_at": "2020-01-05 13:42:19.324003",
    "url": "https://api.chucknorris.io/jokes/ae-78cogr-cb6x9hluwqtw",
    "value": "There is no Esc key on Chuck Norris' keyboard, because no one escapes Chuck Norris."
  }
]
jokes.json

이 JSON 파일을 구문분석하고 농담 목록을 간단한 HTML 페이지에 만들고 싶다면 어떻게 해야 할까요?

예시:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
    <meta name="viewport" content="width=device-width" />
    <title>Fetch Local JSON</title>
  </head>
  <script>
    fetch("./jokes.json", { mode: "no-cors" }) // disable CORS because path does not contain http(s)
      .then((res) => res.json())
      .then((data) => console.log(data));
  </script>
</html>
index.html

위 예시의 HTML 페이지를 만들고 브라우저에서 여는 경우, 콘솔에는 다음과 같은 메시지가 출력됩니다.

Fetch API cannot load file://<path>/jokes.json. URL scheme "file" is not supported

기본적으로 브라우저는 보안상의 이유로 로컬 파일에 대한 액세스를 허용하지 않습니다. 이런 제한은 사용자의 보안과 안전을 위한 것이기 때문에 이 기본 동작을 변경하려고 하면 안 됩니다.

대신 로컬 JSON 파일을 JavaScript로 변환하는 것이 가장 좋은 방법입니다. JSON 문법은 JavaScript와 매우 비슷하므로 이런 변환 작업은 어렵지 않습니다.

먼저 새 파일을 만들고 JSON 데이터를 변수로 선언하기만 하면 됩니다. 다음과 같이 말이죠.

const jokes = [
  {
    "categories": ["dev"],
    "created_at": "2020-01-05 13:42:19.324003",
    "icon_url": "https://assets.chucknorris.host/img/avatar/chuck-norris.png",
    "id": "elgv2wkvt8ioag6xywykbq",
    "updated_at": "2020-01-05 13:42:19.324003",
    "url": "https://api.chucknorris.io/jokes/elgv2wkvt8ioag6xywykbq",
    "value": "Chuck Norris's keyboard doesn't have a Ctrl key because nothing controls Chuck Norris."
  },
  {
    "categories": ["dev"],
    "created_at": "2020-01-05 13:42:19.324003",
    "icon_url": "https://assets.chucknorris.host/img/avatar/chuck-norris.png",
    "id": "ae-78cogr-cb6x9hluwqtw",
    "updated_at": "2020-01-05 13:42:19.324003",
    "url": "https://api.chucknorris.io/jokes/ae-78cogr-cb6x9hluwqtw",
    "value": "There is no Esc key on Chuck Norris' keyboard, because no one escapes Chuck Norris."
  }
]
jokes.js

그런 다음 별도의 스크립트로 HTML 페이지에 추가합니다.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
    <meta name="viewport" content="width=device-width" />
    <title>Fetch Local JSON</title>
  </head>
  <script src="jokes.js"></script>
  <script>
    console.log(jokes);
  </script>
</html>

이제 코드에서 농담 배열을 자유롭게 사용할 수 있습니다.

JavaScript 모듈을 사용해 같은 작업을 수행할 수도 있지만, 이 기사에서는 모듈 사용에 대한 소개는 생략하겠습니다.

하지만 컴퓨터에 Node.js가 설치된 상태에서 로컬 JSON files로 작업하고 싶다면 어떻게 해야 할까요? 이어서 살펴보겠습니다.

Node.js에서 JSON 구문분석하기

Node.js는 브라우저 외부에서 JavaScript를 실행할 수 있는 JavaScript 런타임입니다. Node.js에 대한 소개는 여기(영문 기사)에서 확인할 수 있습니다.

Node.js를 사용하여 컴퓨터에서 로컬로 코드를 실행하든 서버에서 전체 웹 응용 프로그램을 실행하든, 기본적인 Node.js와 JSON 사용법은 알아두는 것이 좋습니다.

다음 예에서는 같은 jokes.json 파일을 사용하겠습니다.

[
  {
    "categories": ["dev"],
    "created_at": "2020-01-05 13:42:19.324003",
    "icon_url": "https://assets.chucknorris.host/img/avatar/chuck-norris.png",
    "id": "elgv2wkvt8ioag6xywykbq",
    "updated_at": "2020-01-05 13:42:19.324003",
    "url": "https://api.chucknorris.io/jokes/elgv2wkvt8ioag6xywykbq",
    "value": "Chuck Norris's keyboard doesn't have a Ctrl key because nothing controls Chuck Norris."
  },
  {
    "categories": ["dev"],
    "created_at": "2020-01-05 13:42:19.324003",
    "icon_url": "https://assets.chucknorris.host/img/avatar/chuck-norris.png",
    "id": "ae-78cogr-cb6x9hluwqtw",
    "updated_at": "2020-01-05 13:42:19.324003",
    "url": "https://api.chucknorris.io/jokes/ae-78cogr-cb6x9hluwqtw",
    "value": "There is no Esc key on Chuck Norris' keyboard, because no one escapes Chuck Norris."
  }
]
jokes.json

require()로 JSON 파일 구문분석하기

가장 쉬운 방법부터 시작해보겠습니다.

로컬 JSON 파일이 있다면 require()를 사용해 다른 Node.js 모듈처럼 로딩하기만 하면 됩니다.

const jokes = require('./jokes.json');

JSON 파일이 자동으로 구문분석되고 프로젝트에 이 파일을 바로 사용할 수 있습니다.

const jokes = require('./jokes.json');

console.log(jokes[0].value); // "Chuck Norris's keyboard doesn't have a Ctrl key because nothing controls Chuck Norris."

이 작업은 동기적으로(synchronous) 처리되므로 전체 파일의 구문분석이 완료할 때까지 프로그램이 일시 중지되기 때문에 JSON 파일이 크면 프로그램 속도가 느려질 수 있다는 점을 주의하세요.

또한 JSON을 이 방법으로 구문분석하면 파일 전체가 메모리에 로딩되므로 정적 JSON 파일에만 이 방법을 사용하는 것이 좋습니다. 프로그램이 실행되는 동안 JSON 파일이 변경된다면 해당 프로그램을 다시 시작하고 업데이트된 JSON 파일을 구문분석할 때까지 변경 사항에 접근할 수 없습니다.

fs.readFileSync()JSON.parse()를 사용해 JSON 파일 구문분석하기

두 번째 방법은 Node.js 프로젝트에서 JSON 파일을 구문분석하는 더 보편적인 방법입니다. fs(파일 시스템) 모듈로 파일을 읽은 다음 JSON.parse()로 구문분석합니다.

fs.readFileSync() 메서드를 사용해 이 작업을 실행하는 방법에 대해 알아보겠습니다. 먼저 프로젝트에 fs 모듈을 추가합니다.

const fs = require('fs');

그런 다음 jokes.json 파일의 출력된 내용을 저장할 새 변수를 만들고 fs.readFileSync() 메서드를 할당합니다.

const fs = require('fs');
const jokesFile = fs.readFileSync();

fs.readFileSync() 메서드는 몇 가지 인수를 사용합니다. 첫 번째 인수는 읽을 파일의 경로입니다.

const fs = require('fs');
const jokesFile = fs.readFileSync('./jokes.json');

지금 jokesFile을 콘솔에 출력한다면, 다음과 같은 내용이 표시됩니다.

<Buffer 5b 0a 20 20 7b 0a 20 20 20 20 22 63 61 74 65 67 6f 72 69 65 73 22 3a 20 5b 22 64 65 76 22 5d 2c 0a 20 20 20 20 22 63 72 65 61 74 65 64 5f 61 74 22 3a ... 788 more bytes>

이런 내용은 fs 모듈이 파일을 읽고 있지만 파일의 인코딩이나 형식을 알지 못한다는 것을 의미합니다. fs은 JSON과 같은 텍스트 기반 파일뿐만 아니라 거의 모든 파일을 로딩하는 데 사용될 수 있으므로 해당 파일이 어떻게 인코딩되는지 알려줘야 합니다.

텍스트 기반 파일의 경우 인코딩은 일반적으로 utf8입니다.

const fs = require('fs');
const jokesFile = fs.readFileSync('./jokes.json', 'utf8');

이제 jokesFile를 콘솔에 출력하면 파일의 내용이 표시됩니다.

하지만 로딩이 완료된 파일은 여전히 문자열입니다. 다른 메서드를 사용해 jokesFile을 JavaScript 객체 또는 배열로 구문분석해야 하는데, 이때 JSON.parse()를 사용하면 됩니다.

const fs = require('fs');
const jokesFile = fs.readFileSync('./jokes.json', 'utf8');
const jokes = JSON.parse(jokesFile);

console.log(jokes[0].value); // "Chuck Norris's keyboard doesn't have a Ctrl key because nothing controls Chuck Norris."

메서드 이름에서 알 수 있듯이 JSON.parse()는 JSON 문자열을 사용해 JavaScript 객체 리터럴 또는 배열로 구문분석합니다.

위의 require 메서드와 마찬가지로 fs.readFileSync()는 동기 메서드입니다. 즉, 큰 파일을 처리해야 할 때 프로그램이 느려질 수 있습니다.

fs.readFileSync()JSON.parse()를 사용해 JSON 파일 구문분석하는 방법 또한 파일을 한 번만 읽고 메모리에 로딩합니다. 파일이 도중에 변경된다면 어느 시점에서 파일을 다시 읽어야 합니다. 파일을 쉽게 다시 읽을 수 있는 간단한 기능을 만드는 것이 좋겠죠. 다음과 같이 말이에요.

const fs = require('fs');
const readFile = path => fs.readFileSync(path, 'utf8');

const jokesFile1 = readFile('./jokes.json');
const jokes1 = JSON.parse(jokesFile1);

console.log(jokes1[0].value); // "Chuck Norris's keyboard doesn't have a Ctrl key because nothing controls Chuck Norris."

// jokes.json 파일이 어느 지점에서 변경됩니다 

const jokesFile2 = readFile('./jokes.json');
const jokes2 = JSON.parse(jokesFile2);

console.log(jokes2[0].value); // "Chuck Norris's keyboard is made up entirely of Cmd keys because Chuck Norris is always in command."

fs.readFile()JSON.parse()를 사용해 JSON 구문분석하기

fs.readFile() 메서드는 fs.readFileSync()와 매우 비슷하지만, 비동기적으로 작동한다는 중요한 차이점이 있습니다. 읽으려고 하는 파일 크기가 크고 로딩 작업이 진행되는 동안 나머지 코드의 실행이 지연되지 않도록 하려면 fs.readFile()을 매우 유용하게 사용할 수 있습니다.

간단한 예시를 봅시다.

const fs = require('fs');

fs.readFile('./jokes.json', 'utf8');

jokesFile과 같은 변수에 메서드를 할당하지 않는다는 점을 제외하면 지금까지 fs.readFileSync()를 사용한 작업과 유사합니다. fs.readFile()은 비동기식이기 때문에 이후의 코드는 파일 읽기가 끝나기 전에 실행됩니다.

fs.readFileSync()와 또 다른 점은 바로 fs.readFile()는 JSON 구문분석하는 콜백 함수를 사용해 내부에 있는 JSON을 구문분석한다는 것입니다.

const fs = require('fs');

fs.readFile('./jokes.json', 'utf8', (err, data) => {
  if (err) console.error(err);
  const jokes = JSON.parse(data);

  console.log(jokes[0].value);
});

console.log("이게 먼저 실행됩니다!");

그러면 콘솔에 다음 내용이 출력됩니다.

이게 먼저 실행됩니다!
Chuck Norris's keyboard doesn't have a Ctrl key because nothing controls Chuck Norris.

fs.readFileSync()와 마찬가지로 fs.readFile()도 파일을 메모리에 로딩하므로 파일이 변경되면 다시 읽어야 합니다.

또한 fs.readFile()이 비동기적으로 작동하지만, 작업이 완료되면 읽히는 전체 파일을 메모리로 로딩한다는 점은 fs.readFile()과 같습니다. 대용량 파일이 있는 경우 대신 Node.js 스트림(stream)을 사용하는 것을 추천합니다.

Node.js에서 JSON.stringify()를 사용해 JSON 문자열로 변환하기

마지막으로, Node.js로 JSON을 구문분석하면 API 응답으로 JSON을 반환해야 할 수도 있습니다.

다행히 이 작업은 브라우저와 같은 방식으로 작동합니다. JSON.stringify()를 사용해 JavaScript 객체 리터럴 또는 배열을 JSON 문자열로 변환하면 됩니다.

const newJoke = {
  categories: ['dev'],
  value: "Chuck Norris's keyboard is made up entirely of Cmd keys because Chuck Norris is always in command."
};

console.log(JSON.stringify(newJoke)); // {"categories":["dev"],"value":"Chuck Norris's keyboard is made up entirely of Cmd keys because Chuck Norris is always in command."}

이렇게 지금까지 브라우저 및 Node.js 프로젝트에서 JSON 관련 작업하는 데 필요한 여러 방법에 대해 살펴보았습니다.

이제 나가서 JSON을 마음껏 구문분석하거나 문자열화하세요!

제가 이 기사에서 빠트린 내용이 있거나, 독자분께서 선호하시는 JSON을 구문분석하는 방법이 있다면, 트위터를 통해 알려주세요!