React UI 상태를 URL에 저장해야 하는 방법과 이유


원문: Sidney Alcantara, https://betterprogramming.pub/how-and-why-you-should-store-react-ui-state-in-the-url-f2013a204cb2

많은 기능, 모달 창 또는 사이드 패널을 가진 복잡한 웹앱을 사용해 본 적이 있는가? 화면을 몇 번 클릭한 후에 원하는 정보가 있는 상태가 되었는데, 실수로 탭을 닫은 적이 있지 않은가? (또는 Windows 업데이트를 한 적이 있지 않은가?)

똑같은 지루한 과정을 거치지 않고 이 상태로 돌아갈 방법이 있다면 좋을 것이다. 아니면 그 상태를 공유해서 팀원이 당신과 동일한 작업을 할 수 있다.

이 문제는 현재 모바일 앱에서 앱을 특정 페이지나 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)만 공유해도 동일한 검색 쿼리를 공유할 수 있다는 것이다. 그리고 이 패러다임은 웹 사용자들에게 지극히 자연스러우며 링크 공유는 늘 월드와이드 웹 일부였다!

example1

이미지 출처: 필자

SPA가 등장했을 때 화면에 표시되는 내용을 변경하기 위해 더 서버 요청을 할 필요가 없기 때문에 이 데이터를 URL에 저장할 필요가 없었다. (single-page라고 부르는 이유이다). 그러나 이로 인해 웹의 고유한 경험인 공유 가능한 링크를 쉽게 잃게 되었다.

데스크톱 및 모바일 앱에는 앱의 특정 부분을 링크하는 표준화된 방법이 없었으며, 딥 링크의 최신 구현은 웹의 URL에 의존한다. 그래서 우리가 네이티브 앱과 같은 기능을 하는 웹앱을 만들 때, 수십 년 동안 가지고 있던 URL의 딥 링크 기능을 왜 버려야 할까?

진짜 단순한 딥 링킹

여러 페이지가 있는 웹앱을 빌드할 때 최소한 /login/home과 같이 다른 페이지가 표시될 때는 URL을 변경해야 한다. React 생태계에서 React Router는 이와 같은 클라이언트 측 라우팅에 이상적이며, Next.js는 서버 측 렌더링도 지원하는 모든 기능을 갖춘 뛰어난 리액트 프레임워크다.

하지만 몇 번의 클릭과 키보드 입력 후 UI 상태까지 이어지는 딥 링크에 관해 이야기하고 있다. 이는 앱을 닫거나 다른 사람과 공유한 후에도 정확한 위치로 바로 돌아갈 수 있고 마찰 없이 업무를 시작할 수 있어 생산성 중심 웹앱의 킬러 기능(역자주: 고유 판매 포인트)이다.

example2

모달이 열리면서 #modal="webhooks"를 추가하기 위해 URL이 어떻게 업데이트 되는지 확인하라.

query-string과 같은 npm 패키지를 사용하고 기본 React Hook을 작성하여 URL 쿼리 파리미터를 상태에 동기화할 수 있으며, 이에 대한 많은 튜토리얼(1, 2, 3)이 많이 있지만, 더 간단한 솔루션이 있다.

React 앱 Rowy의 아키텍처 재작성을 위해 React의 최신 상태 관리 라이브러리를 탐색하던 중, React 팀의 Recoil 라이브러리를 기반으로 하는 작은 원자 기반 상태 라이브러리인 Jotai를 우연히 발견했다.

이 모델의 주요 이점은 상태 원자가 구성 요소 계층 구조로부터 독립적으로 선언되고 앱의 어디에서나 조작할 수 있다는 것이다. 이는 불필요한 리렌더링을 일으키는 React 컨텍스트 문제를 해결한다. 필자는 이전에 useRef를 활용하여 문제를 해결했다. Atomic 상태 개념에 대한 자세한 내용은 Jotai의 문서에서, 좀 더 기술적인 버전은 Recoil의 문서에서 읽을 수 있다.

코드

Jotai에는 상태 원자를 URL 해시와 동기화하는 atomWithHash라는 atom 타입이 있다.
URL에 모달의 열린 상태를 저장하는 것을 원한다고 가정한다. atom을 생성하여 시작하겠다.

import { atomWithHash } from "jotai/utils";

export const modalOpenAtom = atomWithHash("modalOpen", false);

그런 다음 모달 컴포넌트 자체에서 useState같이 useAtom을 사용해 이 atom를 사용할 수 있다.

import { useAtom } from "jotai";
import { modalOpenAtom } from "./atoms";

function ExampleModal() {
  const [open, setOpen] = useAtom(modalOpenAtom);
  return (
    <Dialog
      open={open}
      onClose={() => setOpen(false)}
      ...
    />
  )
}

이는 다음과 같이 표현된다.

이미지

그리고 그게 전부다! 간단하다.

JotaiatomWithHashuseState가 사용하는 모든 데이터를 저장할 수 있고 URL에 저장할 객체를 자동으로 문자열화해서 더 복잡한 상태를 URL에 저장할 수 있어 공유가 가능하다는 점이 환상적이다.

Rowy에서는 이 기술을 사용하여 클라우드 로그용 UI를 구현한다. 우리는 백엔드 개발을 더 쉽게 만들고 공통 워크플로우에 대한 마찰을 제거하는 오픈 소스 플랫폼을 구축하고 있다. 그래서 로그 공유를 위한 마찰을 줄이는 방법은 우리가 완벽하게 필요로 하는 기능이었다. 특정 배포 로그에 연결할 수 있는 데모에서 이 작업을 볼 수 있다.

https://demo.rowy.io/table/roadmap#modal="cloudLogs"&cloudLogFilters={"type":"build","timeRange":{"type":"days","value":7},"buildLogExpanded":1}

배포 로그

URL 구성 요소를 디코딩하면 리액트에서 사용된 정확한 상태가 표시된다.

cloudLogFilters = {
  "type": "build",
  "timeRange": { "type": "days", "value": 7 },
  "buildLogExpanded": 1
}

atomWithHash의 사이드 이펙트은 기본적으로 상태를 브라우저 기록으로 푸시하므로 사용자가 뒤로 및 앞으로 버튼을 클릭하여 UI 상태 간에 이동할 수 있다는 것이다.

이미지

이 동작은 선택 사항이며 replaceState 옵션을 사용하여 비활성화할 수 있다.

const modalOpenAtom = atomWithHash("modalOpen", false, {
  replaceState: true,
});

읽어줘서 고맙다! 특히 구현이 쉽기 때문에 URL에 UI 상태를 더 많이 노출하여 쉽게 공유할 수 있고 사용자의 마찰을 줄일 수 있기를 바란다.