URL에 데이터 구겨 넣기
한정된 2KB 공간에 데이터를 잘 눌러 담아 보자.
- 최초 게시
- 2023년 1월 4일 13시
- 키워드
- URL,
- Frontend, CSV, JSON
웹에서 꾸미는 크리스마스 트리
Repository (미완..): https://github.com/yejunian/christmas-tree
2021년 말쯤에, 크리스마스 트리를 이모지만으로 만들어서 공유할 수 있게 해 보면 재미있겠다는 생각을 했다. 그래서 어떻게 만들면 좋을지 구상을 해 나가기 시작했다.
기본 구상
초기에 구상한 기본적인 흐름은 다음과 같았다.
- 이모지로 트리를 꾸민다.
- URL을 공유한다.
- 다른 사람이 그 URL을 타고 들어와서 트리를 구경한다. (방문자에게 보여줄 메시지도 있으면 좋을 것 같다.)
- 트리를 구경하던 사람이 그 트리를 이어서 꾸미거나 새 트리를 꾸미고, URL을 공유한다.
질문과 대답
여기서 질문을 만들고 답해 보았다.
트리 꾸미기 관련 질문(여기서는 별로 안 중요함)
- 데이터: 어떤 속성을 조작해서 트리를 꾸밀 것인가?
- 기본 구조는 어떻게 할까? → 장식물 리스트, 버전 정보
- 장식물 필수(기초) 속성으로는 무엇을 둘까? → 장식 텍스트, 위치(x, y)
- 장식물 선택(부가) 속성으로는 무엇을 둘까? → 크기, 색상(일반 텍스트 입력 시), 회전 각도
- 사용자 인터페이스(UI): 어떻게 속성을 조작해서 트리를 꾸밀 것인가?
- 어떤 뷰가 필요할까? → 크게 보기(기본), 장식물 목록, 개별 장식물 편집, 공유
- 각 뷰에서 사용자가 (트리 구경 이외에) 기본적으로 어떤 행동을 할 수 있어야 할까?
- 크게 보기 뷰 → 편집(장식물 목록 뷰 진입)
- 장식물 목록 뷰 → 장식물 목록 열람, 새 장식물 추가, 개별 장식물 편집(개별 장식물 편집 뷰 진입), 공유(공유 뷰 진입)
- 개별 장식물 편집 뷰 → 장식물 복제·삭제, 장식물 속성 수정, 완료(장식물 목록 뷰 진입)
- 공유 뷰 → 꾸민 트리 URL 공유, 빈 트리 URL 공유, 완료(장식물 목록 뷰 진입)
URL 공유 관련 질문
- 프론트엔드와 정적 웹 호스팅만으로, 꾸민 트리의 URL 공유가 가능할까? → URL의 쿼리스트링을 활용한다.
- 데이터를 공유 URL에 어떻게 넣고 URL에서 어떻게 읽어들일까? → 고민
- 어떤 형식으로 공유용 데이터를 기술할 것인가? → JSON은 테이블 형태의 데이터를 표현하는 경우 반복되는 내용이 많다. URL 길이는 한정적일 수 있으므로, JSON보다 더 짧은 포맷을 고려해야 할 수 있다.
- URL-safe하지 않은 문자에 퍼센트 인코딩이 적용되면 길이가 매우 길어지는데(최대 300%), 그 길이를 줄일 수 있을까? → Base64URL로 인코딩한다. (약 133% + 1바이트 이내)
고민: URL에 데이터를 구겨 넣을 방법
데이터를 한정된 공간 안에 상황에 맞는 방법으로 넣을 방법을 찾고자 한다. (‘코끼리를 냉장고에 넣는 방법’이 떠오른다.) 일단 내가 알고 있는 제약사항은 다음과 같았다.
- URL은 2000바이트 이내일 때 잘 작동한다.
- URL-safe하지 않은 문자에는 퍼센트 인코딩(영문)이 적용된다. 해당 문자는 각 바이트가
%xx
꼴로 변형되어 3배 크기를 차지하게 된다.
일반적으로 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
- 1바이트 문자 중 데이터에 포함되지 않을 만한 것(예:
그래서 공유 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
,u
로x
,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 미만의 실수v
로a(v) = 360 * (1 - acos(2 * v - 1) / PI) - 179
를 구하여 소수점 아래 첫째 자리에서 버림한다.
- 위 방법을 바탕으로 TSV와 JSON을 생성하여, 여러 가지 인코딩을 적용하여 저장한다. - TSV 헤더에서, 표시 텍스트는
t
, 위치는x
와y
, 크기는s
, 색상은c
, 회전 각도는a
이다. - JSON은{ t: string, x: number, y: number, s: number, c: string, a: number }[]
형식으로 생성한다.
다음은 데이터 포맷과 압축, 인코딩 방법에 따른 비교이다. (개별 데이터 크기 표)
인코딩 | JSON | TSV (vs Raw JSON) | TSV (vs Raw TSV) |
---|---|---|---|
Raw data (UTF-8) | 100.0% | 49.1% - 51.0% | 100.0% |
Percent encoding | 218.6% - 221.9% | 87.4% - 91.8% | 176.2% - 181.8% |
Base64URL | 133.3% - 133.4% | 65.5% - 68.0% | 133.3% - 133.5% |
Brotli + Base64URL | 22.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에 넣는 방법을 생각하여 구현해 보았다.