이모지로 꾸민 크리스마스 트리에서 시작하여 URL로 향하는 화살표
[메인 포스트]

URL에 데이터 구겨 넣기

한정된 2KB 공간에 데이터를 잘 눌러 담아 보자.

최초 게시
2023년 1월 4일 13시
키워드
URL,
Frontend, CSV, JSON

웹에서 꾸미는 크리스마스 트리

Repository (미완..): https://github.com/yejunian/christmas-tree

2021년 말쯤에, 크리스마스 트리를 이모지만으로 만들어서 공유할 수 있게 해 보면 재미있겠다는 생각을 했다. 그래서 어떻게 만들면 좋을지 구상을 해 나가기 시작했다.

기본 구상

초기에 구상한 기본적인 흐름은 다음과 같았다.

  1. 이모지로 트리를 꾸민다.
  2. URL을 공유한다.
  3. 다른 사람이 그 URL을 타고 들어와서 트리를 구경한다. (방문자에게 보여줄 메시지도 있으면 좋을 것 같다.)
  4. 트리를 구경하던 사람이 그 트리를 이어서 꾸미거나 새 트리를 꾸미고, URL을 공유한다.

질문과 대답

여기서 질문을 만들고 답해 보았다.

트리 꾸미기 관련 질문(여기서는 별로 안 중요함)
  • 데이터: 어떤 속성을 조작해서 트리를 꾸밀 것인가?
    • 기본 구조는 어떻게 할까? → 장식물 리스트, 버전 정보
    • 장식물 필수(기초) 속성으로는 무엇을 둘까? → 장식 텍스트, 위치(x, y)
    • 장식물 선택(부가) 속성으로는 무엇을 둘까? → 크기, 색상(일반 텍스트 입력 시), 회전 각도
  • 사용자 인터페이스(UI): 어떻게 속성을 조작해서 트리를 꾸밀 것인가?
    • 어떤 뷰가 필요할까? → 크게 보기(기본), 장식물 목록, 개별 장식물 편집, 공유
    • 각 뷰에서 사용자가 (트리 구경 이외에) 기본적으로 어떤 행동을 할 수 있어야 할까?
      • 크게 보기 뷰 → 편집(장식물 목록 뷰 진입)
      • 장식물 목록 뷰 → 장식물 목록 열람, 새 장식물 추가, 개별 장식물 편집(개별 장식물 편집 뷰 진입), 공유(공유 뷰 진입)
      • 개별 장식물 편집 뷰 → 장식물 복제·삭제, 장식물 속성 수정, 완료(장식물 목록 뷰 진입)
      • 공유 뷰 → 꾸민 트리 URL 공유, 빈 트리 URL 공유, 완료(장식물 목록 뷰 진입)

URL 공유 관련 질문

  • 프론트엔드와 정적 웹 호스팅만으로, 꾸민 트리의 URL 공유가 가능할까? → URL의 쿼리스트링을 활용한다.
  • 데이터를 공유 URL에 어떻게 넣고 URL에서 어떻게 읽어들일까? → 고민
    • 어떤 형식으로 공유용 데이터를 기술할 것인가? → JSON은 테이블 형태의 데이터를 표현하는 경우 반복되는 내용이 많다. URL 길이는 한정적일 수 있으므로, JSON보다 더 짧은 포맷을 고려해야 할 수 있다.
    • URL-safe하지 않은 문자에 퍼센트 인코딩이 적용되면 길이가 매우 길어지는데(최대 300%), 그 길이를 줄일 수 있을까? → Base64URL로 인코딩한다. (약 133% + 1바이트 이내)

고민: URL에 데이터를 구겨 넣을 방법

데이터를 한정된 공간 안에 상황에 맞는 방법으로 넣을 방법을 찾고자 한다. (‘코끼리를 냉장고에 넣는 방법’이 떠오른다.) 일단 내가 알고 있는 제약사항은 다음과 같았다.

일반적으로 Base64로 인코딩하면 데이터 길이가 약 133%(4/3배)로 늘어난다. Base64 인코딩에는 +, /, =가 쓰여서 여기에도 퍼센트 인코딩이 적용된다. Base64에서 패드 =를 삭제하고, +, /를 각각 -, _로 치환하면, URL-safe한 결과물(Base64URL)을 얻을 수 있다. 이때 원본 데이터의 URL-safe한 문자 비중이 일정량 이하라면, 퍼센트 인코딩보다 더 짧은 결과가 나온다. 따라서 가장 마지막 단계에는 Base64URL 인코딩을 적용하기로 했다.

이어서 적절한 기초 데이터 기술 방법을 정하기 위해, 몇 가지 방법에 대한 생각을 정리해 보았다.

  • JSON은 구조상 반복되는 문자가 많다. 특히 테이블을 JSON으로 { column1: Type1, column2: Type2, ... }[]와 같이 표현하면 매 행에 프로퍼티 이름이 반복되어 나타난다.
    • [
        { "t": "🌲", "x": 0,    "y": 0,    "s": 16,  "c": "#E1C435", "a": 0   },
        { "t": "🔔", "x": -1.3, "y": -3.8, "s": 1.2, "c": "#E1C435", "a": -11 },
        { "t": "⭐️", "x": 0.6,  "y": -3.6, "s": 1,   "c": "#E1C435", "a": -53 },
        { "t": "💭", "x": 4.2,  "y": 1.9,  "s": 1.4, "c": "#E1C435", "a": -39 }
      ]
    • 헤더를 포함한 2차원 배열로 표현한다면 프로퍼티 이름 중복은 없겠지만, CSV와 크게 다를 바가 없어 보인다.
  • 온라인 저지에서 텍스트 입력 받는 것처럼 독자 포맷을 적용하면, 명세서를 따로 작성해야 하고, 나조차도 매번 헷갈릴 것이다. 나중에 명세가 변경되기라도 하면 더 많은 노력과 비용이 필요할 것이다.
  • CSV는 테이블 형태의 데이터를 표현하기에 적합하다. 정해진 프로퍼티만 들어간 { prop1: Type1, prop2: Type2, ... }[]와 같은 형식의 데이터라면 CSV로 쉽게 표현할 수 있을 것이다.
    • 1바이트 문자 중 데이터에 포함되지 않을 만한 것(예: '\t', '\n')을 구분자로 사용하면 데이터의 길이가 조금이라도 더 짧아질 것이다.
    • t	x	y	s	c	a
      🌲	0	0	16	#E1C435	0
      🔔	-1.3	-3.8	1.2	#E1C435	-11
      ⭐️	0.6	-3.6	1	#E1C435	-53
      💭	4.2	1.9	1.4	#E1C435	-39

그래서 공유 URL에 넣을 데이터는 TSV로 뽑아내기로 했다.

한편, 트리를 장식하다 보면 같은 속성 값을 가진 장식물을 여럿 쓰게 된다. 왠지 압축을 하면 좋을 것 같다는 생각이 들었다. Brotli는 웹 폰트나 서버 쪽 데이터 압축에도 쓰이고 있고, MIT 라이선스로 배포되고 있고, 웹 프론트엔드에서 쓸 수 있는 WASM 버전의 패키지가 npm에 올라와 있기도 하다(양방향, Decompressor only). 따라서 Brotli 압축을 적용해 보기로 했다.

비교: 실제로는 어떨까

고민해서 생각해 본 방법들의 효율성을 비교해 보았다. 개발 단계에서는 샘플 데이터를 자동으로 뽑아낼 생각까지는 안 했어서, 간략하게만 확인해 보았었다. 여기에 올린 비교는 생각해 본 방법의 효율성을 확인해 보기 위해서 샘플 데이터를 생성하여 진행한 것이다.

샘플 데이터 생성 방법

샘플 데이터 생성(100개) 구현 코드, 생성된 데이터: https://gist.github.com/yejunian/6859bc5a66aa7f39e15bbe838bd3c15b

  • 첫 번째 장식물은 정가운데에 있는 크기 16짜리 상록수 이모지(🌲, U+1F332)이다.
  • 그 외 장식물은 다음과 같은 방법으로 생성한다.
    • 표시 텍스트는 트리에 사용할 만한 이모지 40종, 필기체 영문 대소문자 52종(U+1D4D0 - U+1D503) 중에서 무작위로 선정한다. 단, 이모지와 알파벳의 비율을 제한한다.
    • 위치는 세 점 (0, -8), (-7, 6), (7, 6)을 꼭짓점으로 하는 (밑변, 높이가 모두 14인) 이등변삼각형에 장식물이 걸릴 것으로 가정하여, 여기에 장식물이 고르게 배치될 수 있도록 결정한다. 무작위로 뽑은 0 이상, 1 미만의 실수 t, ux, y를 다음과 같이 구하고, 두 값을 모두 구한 뒤에 소수점 아래 둘째 자리에서 버림한다.
      • y(t) = (t ** (1 / 2)) * 14.1 - 8
      • x(t, u) = (y(t) + 8) * (u - 7 / 14.1)
    • 크기는 0.1 이상, 2.6 미만의 실수를 무작위로 뽑아 소수점 아래 둘째 자리에서 버림하여 구한다.
    • 색상은, 표시 텍스트가 이모지인 경우 노란색으로 강제하고, 아닌 경우 노란색을 포함한 12가지 색상 중에서 무작위로 선정한다.
    • 회전 각도 a는 무작위로 뽑은 0 이상, 1 미만의 실수 va(v) = 360 * (1 - acos(2 * v - 1) / PI) - 179를 구하여 소수점 아래 첫째 자리에서 버림한다.
  • 위 방법을 바탕으로 TSV와 JSON을 생성하여, 여러 가지 인코딩을 적용하여 저장한다. - TSV 헤더에서, 표시 텍스트는 t, 위치는 xy, 크기는 s, 색상은 c, 회전 각도는 a이다. - JSON은 { t: string, x: number, y: number, s: number, c: string, a: number }[] 형식으로 생성한다.

다음은 데이터 포맷과 압축, 인코딩 방법에 따른 비교이다. (개별 데이터 크기 표)

인코딩JSONTSV (vs Raw JSON)TSV (vs Raw TSV)
Raw data (UTF-8)100.0%49.1% - 51.0%100.0%
Percent encoding218.6% - 221.9%87.4% - 91.8%176.2% - 181.8%
Base64URL133.3% - 133.4%65.5% - 68.0%133.3% - 133.5%
Brotli + Base64URL22.8% - 48.5%21.1% - 41.5%42.6% - 82.2%
  • TSV의 크기는 JSON의 1/2 정도였다. 데이터에 들어가는 값이 길지 않아서 문법적 요소의 비중이 크기 때문에 큰 차이가 나는 것 같다.
  • 데이터에 아무 처리도 하지 않아서 퍼센트 인코딩이 적용되는 경우, JSON은 약 2.2배, TSV는 약 1.8배 정도로 크기가 불어났다. Base64URL로 인코딩했을 때보다 더 크다.
  • 원본을 Brotli로 압축한 경우, JSON 쪽의 압축률이 더 좋았다. 그래도 TSV를 압축한 크기가 JSON의 것보다 더 작게 나타났다. 값이 길어져서 문법적 요소의 비중이 줄어들거나, 기본값이 들어간 프로퍼티가 생략된다면, 양쪽이 비슷해질 수도 있겠다.

나는 2KB 공간 안에 조금이라도 더 많은 정보를 넣을 수 있게 하고 싶었기 때문에, TSV 포맷의 트리 장식물 데이터를 Brotli로 압축한 뒤 Base64URL로 인코딩하기로 했다.

이 비교의 한계

데이터를 생성할 때, JSON에서 필수가 아닌 프로퍼티를 전혀 고려하지 않았다. 따라서 실제 사용자들이 생성하게 될 데이터와는 거리가 좀 있다. 더 올바르게 비교하려면 다음 내용이 반영되어야 한다.

  • 필수가 아니면서 기본값이 적용되는 프로퍼티(크기, 글자 색상, 회전 각도)를 생략한다.
  • 사용자가 일부 항목을 기본값 그대로 두는 것을 반영한다. 필수가 아닌 프로퍼티의 일정 비율을 기본값으로 채운다.

마무리

서버 단에서 아무것도 하지 않을 생각이거나 아무것도 할 수 없는 상황에서 (보안이 필요하지 않은) 2000자가 넘을 만한 긴 데이터가 공유되어야 한다면, 데이터를 표현할 다양한 포맷과 압축 방법과 인코딩을 잘 조합하여 URL에 넣는 방법을 고려해 볼 수 있겠다. 나는 트리 장식물 목록을 TSV 포맷으로 표현하여 Brotli로 압축하고 Base64URL로 인코딩하여 URL에 넣는 방법을 생각하여 구현해 보았다.

아, 그리고 저 미완성인 ‘이모지로 꾸미는 크리스마스 트리’는 돌아오는 12월에는 쓸 수 있도록 완성해 놓아야겠다.

yejunian / blog

© 2022-2024 yejunian

라이선스를 따로 명시하지 않았다면, 각 콘텐츠에는 아래와 같은 라이선스가 적용됩니다.

  • yejunian/blog-post에 업로드한 게시물(코드 블록 제외): CC-BY 4.0
  • 게시물에 삽입된 코드 블록의 내용: 퍼블릭 도메인
  • 댓글 등, 웹 사이트 방문자가 작성한 게시물의 저작권은 그 콘텐츠의 작성자에게 있으며, 이를 활용하려면 저작권자의 이용허락이 필요합니다.

게시물에 포함된 이미지 등을 외부 서비스에서 사용하려면, 해당 콘텐츠를 다운로드한 뒤 공유하려는 서비스의 콘텐츠 첨부 기능을 활용하기 바랍니다. 이 사이트에 첨부한 파일의 URL은 언제든지 변경될 수 있습니다.

Powered by Gatsby. Hosted on GitHub.