🐌
왜 JSON.parse로 객체를 선언하는 방법이 더 빠를까?

JavaScript 객체 리터럴(Object literal)보다 JSON.parse로 파싱하는 것이 더 나은 성능을 보이는 이유에 대해 알아봅니다.

December 04, 2019


JavaScript


이 글은 Google Chrome Developer의 영상 『Faster apps with JSON.parse』를 번역 및 의역한 것임을 밝힙니다.

오늘은 여러분이 기존에 알고 있던 상식에서 벗어나는 JavaScript의 성능과 관련된 신비로운 트릭을 하나 소개하고자 합니다.

const data = {
  foo: 42,
  bar: 1337,
  ...
}; // 10 kB+

일반적으로 JavaScript를 활용한 웹 어플리케이션에서는 상태나 기타 데이터들을 저장하고 활용하기 위해서 흔히 객체(Object)를 사용하곤 합니다. 우리가 위에서 볼 수 있는 예시는 매우 간단한 객체죠. 하지만 React나 Redux와 같은 프레임워크를 이용해 구현된 웹 어플리케이션의 경우 용량이 수 킬로바이트나 되는 큰 객체를 쉽게 볼 수 있습니다.

안타깝게도 웹 어플리케이션들은 대부분 이러한 데이터를 기반으로 DOM을 만들게 됩니다. 따라서 JavaScript 객체는 DOM의 렌더링에 있어 중요한 역할을 하게 되므로 사용자들은 모든 데이터가 JavaScript 엔진에 의해 로드, 파싱, 컴파일 및 실행이 완료되기까지 빈 화면을 마주할 수 밖에 없습니다.

그러면 어떻게 해야 이 과정을 더 빠르게 할 수 있을까요?

이를 해결하기 위한 방법 중 하나는 서버 사이드 렌더링(Server-Side Rendering, SSR) 을 이용하는 것입니다. 서버 사이드 렌더링은 서버에서 데이터의 연산과 처리를 끝낸 후 정적인 HTML 문서만을 사용자에게 제공하므로 웹 어플리케이션의 초기 로드에 JavaScript가 필요하지 않습니다. 즉, JavaScript에서 용량이 큰 객체를 사용하지 않아도 됩니다.

하지만, 만약 서버 사이드 렌더링을 사용할 수 없는 경우에는 어떻게 해야 할까요? 성능 향상을 위한 다른 방법들이 없을까요? 경우에 따라 다릅니다.

일반적으로 데이터를 나타내기 위한 객체의 프로퍼티로 Date, BigInt, Map, Set처럼 JSON 형식으로 명확하게 나타낼 수 없는 값은 잘 사용하지 않습니다.

// 객체 리터럴 방식
const data = {
  foo: 42,
  bar: 1337,
  ...
};

// JSON.parse 메소드를 이용하는 방식
const data = JSON.parse('{"foo":42,"bar":1337,...}');

// 두 예시는 동등한 객체를 생성합니다.

이러한 경우에는 JavaScript 코드에 객체를 직접 선언하는 객체 리터럴(Object literal) 대신, 객체의 내용을 JSON 형태로 직렬화(serialize)하고 JavaScript 문자열(String)으로 변환한 후 내장된 JSON.parse 메소드를 이용하는 방법으로도 객체를 생성할 수 있습니다.

그럼, 이 두 가지 방법 중에서 어떤 것이 더 빠를까요? 신기하게도 JSON.parse로 객체를 생성하는 법이 훨씬 빠르답니다.

JavaScript 객체 리터럴은 좀 더 직접적인 접근 방식인 것 같지만 JSON.parse는 보다 간접적인 방식으로 느껴지기 때문에 이 결과는 조금 의외일 수 있습니다. JSON.parse가 더 나은 성능을 보이는 이유는 크게 두 가지가 있습니다.

JSON이 읽고 분석하기 더 쉽다

JSON.parse('{"foo":42,"bar":1337,...}')

그 첫 번째 이유는 JavaScript 엔진에게 있어서 JSON을 읽고 분석하는 것은 매우 간단하기 때문입니다. JavaScript 파서에게 있어서 위의 코드는 단일 인수(argument)를 갖는 호출식(CallExpression)일 뿐입니다. 즉, 거대한 양의 데이터는 사실 단일 문자열 리터럴(StringLiteral) 토큰으로 여겨질 뿐입니다.

{
  foo: 42,
  bar: 1337,
  ...
}

하지만 이와 다른 방법인 객체 리터럴로 객체를 생성할 경우, 보다 많은 토큰이 필요합니다. 일반적으로 객체의 프로퍼티명은 식별자(Identifier) 토큰 또는 문자열로 취급되는 리터럴로 구성되고 프로퍼티의 값(value)에는 숫자(NumericLiteral)나 문자열(StringLiteral), 불리언(BooleanLiteral), 배열(ArrayLiteral), 객체(ObjectLiteral)처럼 훨씬 다양한 리터럴이 올 수 있습니다. 만약 프로퍼티의 값이 중첩된 객체나 배열일 경우에는 훨씬 더 많은 토큰이 필요하죠.

JSON.parse와 비교했을 때, JavaScript 파서는 객체 리터럴 코드를 올바르게 토큰화하기 위해서 더 많은 리소스를 필요로 합니다.

JavaScript는 경우의 수가 훨씬 많다

JavaScript 객체 리터럴이 읽고 분석하기에 더 어려운 또 다른 이유는, JavaScript 문법 상 파서가 객체 리터럴이라는 것을 쉽게 알아채기가 어렵기 때문입니다. 이게 무엇을 의미하는 말인지 좀 더 자세히 알아보죠.

JSON.parse('{

// 1. 객체의 시작
// JSON.parse('{"foo":42,"bar":1337,...}')

// 2. 유효하지 않은 JSON
// JSON.parse('{"foo"}')

JavaScript 파서의 입장에서 생각해 봅시다. 파서는 문맥을 파악하기 위해 소스 코드를 한 글자씩 읽어 나갑니다. 자, 만약 위처럼 JSON.parse를 처리하는 도중 중괄호가 열리는 코드를 만났다면 파서의 입장에서는 두 가지 선택를 생각할 수 있습니다.

  1. 객체의 시작
  2. 유효하지 않은 JSON

이 외에는 더 이상 생각할 게 없죠. 하지만 JavaScript에서는 훨씬 더 많은 경우의 수가 있습니다. 몇 가지 경우의 수를 살펴봅시다.

const x = 42;

const y = ({ x } // 무엇을 의미할까요?

위의 예시에서 두 번째 줄에 괄호가 닫히지 않은 채 열려 있습니다. 여러분은 이 것이 객체 리터럴인지 아닌지 판단할 수 있나요? 그리고 두 번째 줄의 x가 무엇을 참조하는지 짐작할 수 있나요? 첫 번째 줄의 바인딩을 참조하는 걸까요, 아니면 또 다른 것일까요?

코드의 나머지 부분을 보지 않고는 이러한 질문에 대답을 할 수 없습니다. 나머지 코드를 살펴봅시다.

const x = 42;

const y = ({ x }); // 객체 리터럴

이 경우 두 번째 줄은 객체 리터럴을 만들고, 두 번째 줄의 x는 첫 번째 줄의 변수 x를 참조하는 것을 알 수 있습니다.

const x = 42;

const y = ({ x } = { x: 21 }); // 객체 비구조화

하지만 위와 같은 경우에는 객체 비구조화 문법을 이용한 것이기 때문에 첫 번째 줄을 참조하지 않습니다.

const x = 42;

const y = ({ x }) => x; // ES6 화살표 함수

이 경우는 또 어떨까요? 두 번째 줄이 x로 명명된 파라미터를 비구조화하고 리턴하는 화살표 함수입니다. 이 경우 역시 첫 번째 줄을 참조하지 않습니다.

여기서 말하고자 하는 바는 JavaScript는 문맥에 민감하기 때문에 JavaScript를 파싱하는 것은 까다롭지만, JSON은 그런 문제가 없기 때문에 JSON을 파싱하는 것이 훨씬 간단하다는 것입니다. 따라서 특히 크기가 큰 객체의 경우, JavaScript 객체 리터럴을 사용하는 것보다 JSON.parse를 사용하는 것이 더 빠릅니다.

유의미한 성능 차이가 있을까

아마 여러분들은 대체 얼마나 더 빠른가에 대해 궁금해 하실 것 같습니다. 정확히 얼마나 더 빠른 걸까요?

속도 대부분의 JavaScript 엔진에서 JSON.parse가 더 뛰어난 성능을 보였습니다.

우리는 JavaScript 엔진인 V8와 Chrome에서 8MB 크기의 캐싱되지 않은 객체를 생성했을 때, 약 JSON.parse가 1.7배 더 빠르다는 사실을 발견했습니다. 그리고 이 속도 차이는 다른 JavaScript 엔진 및 브라우저에도 확인할 수 있었습니다(속도 차이는 Safari와 JavaScriptCore에서는 훨씬 두드러졌는데, 거의 2배나 더 빨랐습니다!)

여기에 대해서 성능 측정을 위해 엄청나게 큰 객체를 이용한 테스트 결과에 불과하지 않나? 라고 생각하는 분이 계실 수 있습니다. 이 작은 트릭을 실제 웹 어플리케이션에 어떻게하면 유용하게 활용할 수 있을까요?

실제 어플리케이션에 적용하기

Henrik Joreteg이라는 개발자가 이러한 최적화 방법을 그가 작업하고 있던 Redux 어플리케이션에 적용해보았을 때, 반응 속도(Time to interactive)가 18% 향상되면서 Lighthouse 성능 점수도 8점 정도 상승했다고 발표했습니다. 즉, 단일 객체에 대해서만 최적화를 진행하는 것도 나쁘지 않다는 것입니다!

이 최적화 방법을 어떻게 적용할 수 있을까요? 이 방법을 수동으로 적용하는 방법은 권장하지 않습니다. 소스 코드에서는 객체 리터럴 방식의 가독성이 더 낫기 때문에, 객체 리터럴 방식을 사용하는 것이 낫습니다.

대신에 빌드 도중에 용량이 큰 객체 리터럴을 자동으로 JSON.parse로 변환하는 도구를 사용하는 것이 좋습니다. 웹팩은 여러분의 코드 베이스가 JSON 모듈을 사용하는 경우 JSON.parse로 변환하는 기술을 이미 적용하고 있습니다. 이 외의 다른 코드에도 적용시킬 수 있는 Babel 플러그인도 존재합니다.

세 줄 요약

  • JavaScript에서 객체를 선언할 때 객체 리터럴보다 JSON.parse로 파싱하는 것이 더 빠르다
  • 그 이유는 JSON 문법이 JavaScript 문법보다 간단하고, JavaScript가 제공하는 더 많은 문법으로 인해 파싱에 리소스가 더 많이 들기 때문이다
  • 하지만 적어도 객체의 용량이 8MB는 되어야 유의미한 성능 차이가 나기 때문에, 유의미한 성능 향상을 기대하기는 어렵고 빌드 과정에서 사용해 볼 만 하다.