당신이 리액트 UI 상태를 URL에 저장해야 하는 방법과 이유
리액트 앱에서 UI 상태를 URL에 저장하여 딥링킹과 상태 공유를 구현하는 방법을 소개합니다.
원문: Sidney Alcantara, https://betterprogramming.pub/how-and-why-you-should-store-react-ui-state-in-the-url-f2013a204cb2
당신은 많은 기능, 모달 창 또는 사이드 패널을 가진 복잡한 웹앱을 사용해 본 적이 있습니까? 화면을 몇 번 클릭 후 화면에 올바른 정보만 있는 완벽한 상태에 도달할 수 있지만, 실수로 탭을 닫았습니까? (혹은 윈도우 업데이트하기로 했습니까?)
똑같은 지루한 과정을 거치지 않고 이 상태로 돌아갈 방법이 있다면 좋을 것이다. 아니면 그 상태를 공유할 수 있어서 팀원이 당신과 같은 일을 할 수 있습니다.
이 문제는 현재 모바일 앱에서 앱을 특정 페이지나 UI 상태로 열기 위해 사용하는 딥링크로 해결할 수 있습니다. 하지만 왜 이것은 많은 웹앱에 존재하지 않을까요?
솔루션 및 코드 조각으로 건너뛰려면 여기를 클릭하십시오.
웹에서 딥링크 다시 불러오기
단일 페이지 애플리케이션(SPAs, Single-Page Applications)의 출현으로 웹에서 즉시 상호작용하는 새로운 사용자 경험을 만들 수 있게 되었습니다. Javascript를 사용해 클라이언트 측에서 더 많은 작업을 수행함으로써 사용자 정의 대화창 열기부터 Google Docs와 같은 실시간 텍스트 편집기에 이르기까지 사용자 이벤트에 즉시 응답할 수 있습니다.
기존의 서버 렌더링 웹사이트는 매번 새로운 HTML 페이지를 얻도록 요청을 보냅니다. 가장 좋은 예로는 사용자의 검색 쿼리를 URL (https://www.google.com/search?q=your+query+here)에서 서버로 요청을 보내는 구글이 있습니다. 이 모델의 좋은 점은 지난주 결과를 기준으로 필터링하면 URL (https://www.google.com/search?q=react+js&tbs=qdr:w)만 공유해도 동일한 검색 쿼리를 공유할 수 있다는 것입니다. 그리고 이 패러다임은 웹 사용자들에게 완전히 자연스러운 것입니다. 링크 공유는 발명된 이래로 월드와이드 웹 일부였습니다!

SPA가 등장했을 때 화면에 표시되는 내용을 변경하기 위해 더 서버 요청을 할 필요가 없기 때문에 이 데이터를 URL에 저장할 필요가 없었습니다. (single-page라고 부르는 이유입니다). 그러나 이로 인해 웹의 고유한 경험인 공유 가능한 링크를 쉽게 잃게 되었습니다.
데스크톱 및 모바일 앱에는 앱의 특정 부분에 링크하는 표준화된 방법이 없었으며, 딥 링크의 최신 구현은 웹의 URL에 의존합니다. 그래서 우리가 네이티브 앱과 같은 기능을 하는 웹앱을 만들 때, 수십 년 동안 가지고 있던 URL의 딥 링킹 기능을 왜 버려야 할까요?
진짜 단순한 딥 링킹
여러 페이지가 있는 웹앱을 빌드할 때 최소한 /login 및 /home과 같이 다른 페이지가 표시될 때는 URL을 변경해야 합니다. 리액트 생태계에서 리액트 라우터는 이와 같은 클라이언트 측 라우팅에 이상적이며, Next.js는 서버 측 렌더링도 지원하는 모든 기능을 갖춘 뛰어난 리액트 프레임워크입니다.
하지만 몇 번의 클릭과 키보드 입력 후 UI 상태까지 이어지는 딥 링크에 관해 이야기하고 있습니다. 앱을 닫거나 다른 사람과 공유한 후에도 정확한 위치로 바로 돌아갈 수 있어 마찰 없이 업무를 시작할 수 있어 생산성 중심 웹앱의 킬러 기능(역주: 고유한 판매 포인트)입니다.

query-string과 같은 npm 패키지를 사용하고 기본 리액트 Hook을 작성하여 URL 쿼리 파리미터를 상태에 동기화할 수 있으며, 이에 대한 많은 튜토리얼(1, 2, 3)이 많이 있지만, 더 간단한 솔루션이 있습니다.
리액트 앱 Rowy의 아키텍처 재작성을 위해 리액트의 최신 상태 관리 라이브러리를 탐색하던 중, 리액트 팀의 Recoil 라이브러리를 기반으로 하는 작은 원자 기반 상태 라이브러리인 Jotai를 우연히 발견했습니다.
이 모델의 주요 이점은 상태 원자가 구성 요소 계층 구조로부터 독립적으로 선언되고 앱의 어디에서나 조작할 수 있다는 것입니다. 이것은 이전에 useRef로 해결했던 불필요한 리 렌더링을 유발하는 리액트 컨텍스트를 통해 문제를 해결합니다. 원자 상태 개념에 대한 자세한 내용은 Jotai의 문서에서, 더욱 기술적인 버전은 Recoil의 문서에서 읽을 수 있습니다.
코드
Jotai에는 상태 원자를 URL 해시와 동기화하는 atomWithHash라는 원자 타입이 있습니다.
URL에 모달의 열린 상태를 저장하는 것을 원한다고 가정합니다. 원자를 생성하여 시작하겠습니다.
1import { atomWithHash } from "jotai/utils";2
3export const modalOpenAtom = atomWithHash("modalOpen", false);그런 다음 모달 컴포넌트 자체에서 useState같이 useAtom을 사용해 이 원자를 사용할 수 있습니다.
1import { useAtom } from "jotai";2import { modalOpenAtom } from "./atoms";3
4function ExampleModal() {5 const [open, setOpen] = useAtom(modalOpenAtom);6 return (7 <Dialog8 open={open}9 onClose={() => setOpen(false)}10 ...11 />12 )13}이는 다음과 같이 표현됩니다.

그리고 그게 전부입니다! 간단합니다.
Jotai의 atomWithHash는 useState가 사용하는 모든 데이터를 저장할 수 있고 URL에 저장할 객체를 자동으로 문자열화해서 더 복잡한 상태를 URL에 저장할 수 있어 공유가 가능하다는 점이 환상적입니다.
Rowy에서는 이 기술을 사용하여 클라우드 로그용 UI를 구현했습니다. 우리는 백 엔드 개발을 더 쉽게 만들고 공통 워크플로우에 대한 마찰을 제거하는 오픈 소스 플랫폼을 구축하고 있습니다. 그래서 로그 공유를 위한 마찰을 줄이는 것이 우리에게 완벽했습니다. 특정 배포 로그에 연결할 수 있는 데모에서 이 작업을 볼 수 있습니다.
https://demo.rowy.io/table/roadmap#modal="cloudLogs"&cloudLogFilters={"type":"build","timeRange":{"type":"days","value":7},"buildLogExpanded":1}

URL 구성 요소를 디코딩하면 리액트에서 사용된 정확한 상태가 표시됩니다.
1cloudLogFilters = {2 "type": "build",3 "timeRange": { "type": "days", "value": 7 },4 "buildLogExpanded": 15}atomWithHash의 부작용은 기본적으로 상태를 브라우저 기록으로 푸시하므로 사용자가 뒤로 및 앞으로 버튼을 클릭하여 UI 상태 간에 이동할 수 있다는 것입니다.

이 동작은 선택 사항이며 replaceState 옵션을 사용하여 비활성화할 수 있습니다.
1const modalOpenAtom = atomWithHash("modalOpen", false, {2 replaceState: true,3});읽어 주셔서 감사합니다! 특히 구현이 쉽기 때문에 URL에 UI 상태를 더 많이 노출하여 쉽게 공유할 수 있고 사용자의 마찰을 줄일 수 있도록 하셨기를 바랍니다.
작성자 추가 주석: 복잡한 데이터의 URL 인코딩
많은 검색 필터나 복잡한 상태를 URL로 관리해야 하는 경우, 데이터를 압축하여 인코딩하는 방법도 고려해볼 수 있습니다. 예를 들어 Mermaid 다이어그램이나 코드 블럭처럼 큰 텍스트 데이터를 URL로 공유하는 서비스들이 이런 방식을 사용합니다.
Pako를 이용한 압축 인코딩 구현
복잡한 데이터를 URL에 저장할 때는 다음과 같은 과정을 거칩니다:
- 데이터 직렬화: JSON.stringify()로 객체를 문자열로 변환
- 압축: Pako의 deflate 알고리즘으로 데이터 압축
- Base64 인코딩: 압축된 바이너리 데이터를 Base64로 인코딩
- URL-safe 변환: '+' → '-', '/' → '_' 로 치환하여 URL 안전성 확보
1import pako from 'pako';2
3// 데이터를 URL용으로 압축 인코딩4function encodeStateToURL(state) {5 const json = JSON.stringify(state);6 const compressed = pako.deflate(json, { level: 9 });7 const base64 = btoa(String.fromCharCode.apply(null, compressed));8 // URL-safe 문자로 변환9 return base64.replace(/\+/g, '-').replace(/\//g, '_');10}11
12// URL에서 데이터 복원13function decodeStateFromURL(encoded) {14 // URL-safe 문자를 원래대로 복원15 const base64 = encoded.replace(/-/g, '+').replace(/_/g, '/');16 const binary = atob(base64);17 const bytes = new Uint8Array(binary.length);18 for (let i = 0; i < binary.length; i++) {19 bytes[i] = binary.charCodeAt(i);20 }21 const decompressed = pako.inflate(bytes, { to: 'string' });22 return JSON.parse(decompressed);23}실제 활용 사례
Mermaid Live Editor는 이런 방식으로 복잡한 다이어그램 코드를 URL에 포함시켜 공유합니다. 예를 들어:
https://mermaid.live/edit#pako:eNpVkM...형태로 압축된 다이어그램을 URL에 포함- 61MB의 대용량 상태 데이터도 효율적으로 압축하여 URL로 관리 가능
구현 시 고려사항
- URL 길이 제한: 브라우저와 서버마다 URL 최대 길이가 다름 (일반적으로 2,048자)
- 압축 효율: 작은 데이터는 오히려 압축 후 크기가 늘어날 수 있음
- 성능: 압축/해제 과정의 CPU 비용 고려
- 대안 알고리즘: 데이터 특성에 따라 LZW, LZMA 등 다른 압축 알고리즘이 더 효율적일 수 있음
개발 중에 URL 상태 관리를 구현할 때 이런 점들을 놓치지 않도록 주의하면, 더 강력하고 유연한 딥링킹 기능을 제공할 수 있습니다.