state-in-url 6.0.2 → 6.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.KO.md CHANGED
@@ -1,81 +1,142 @@
1
- [English] | 한국어 | [简体中文](./README.CN.md)
2
-
1
+ [English](./README.md) | 한국어 | [简体中文](./README.CN.md)
3
2
 
4
3
  <div align="center">
5
- <img src="/assets/Logo_big.png?raw=true" alt="state-in-url logo"/>
4
+ <img src="/packages/example-nextjs14/public/Logo_symbol.png" alt="state-in-url 로고" width="120px" />
6
5
 
7
- <div>IDE 자동 완성 및 TS 검증과 함께 관련 없는 React 컴포넌트 간에 상태를 쉽게 공유할 수 있습니다. 번거로움이나 보일러플레이트 없이 사용 가능합니다.</div>
6
+ # State in url
8
7
  </div>
9
8
 
10
-
11
9
  <div align="center">
12
- <hr />
10
+ </div>
13
11
 
12
+ <div align="center">
14
13
 
15
14
  [![npm](https://img.shields.io/npm/v/state-in-url.svg)](https://www.npmjs.com/package/state-in-url)
16
- ![Tests](https://github.com/asmyshlyaev177/state-in-url/actions/workflows/tests.yml/badge.svg?branch=master)
15
+ ![npm bundle size (minified + gzip)](https://img.shields.io/bundlephobia/minzip/state-in-url.svg)
17
16
  [![Codacy Badge](https://app.codacy.com/project/badge/Grade/73be54068b7f41b0b74a252579ac09ec)](https://app.codacy.com/gh/asmyshlyaev177/state-in-url/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
17
+
18
+ ![Tests](https://github.com/asmyshlyaev177/state-in-url/actions/workflows/tests.yml/badge.svg?branch=master)
19
+ [![Codacy Badge](https://app.codacy.com/project/badge/Coverage/73be54068b7f41b0b74a252579ac09ec)](https://app.codacy.com/gh/asmyshlyaev177/state-in-url/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_coverage)
18
20
  [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](https://github.com/asmyshlyaev177/state-in-url/)
19
- ![npm bundle size (minified + gzip)](https://img.shields.io/bundlephobia/minzip/state-in-url.svg)
21
+ [![semantic-release: angular](https://img.shields.io/badge/semantic--release-angular-e10079?logo=semantic-release)]([https://github.com/semantic-release/semantic-release](https://github.com/asmyshlyaev177/state-in-url))
22
+
23
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/asmyshlyaev177/state-in-url/badge)](https://scorecard.dev/viewer/?uri=github.com/asmyshlyaev177/state-in-url)
24
+ [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/9679/badge)](https://www.bestpractices.dev/projects/9679)
25
+ [![license](https://img.shields.io/github/license/asmyshlyaev177/state-in-url.svg?style=flat-square)](https://github.com/asmyshlyaev177/state-in-url/blob/master/LICENSE)
26
+
20
27
  </div>
21
28
 
22
29
  <div align="center">
23
30
 
24
- <h4 align="center">버그를 발견하셨다면 주저하지 마시고 이슈를 열어주세요</h4>
31
+ <h4 align="center">버그를 발견하거나 기능 요청이 있으시면 주저하지 말고 이슈를 열어주세요</h4>
25
32
 
26
- ![데모-gif](https://github.com/asmyshlyaev177/state-in-url/assets/19854148/c9802601-4d42-4362-b3e4-37ff87c3b97f)
33
+ ![Demo-gif](https://github.com/asmyshlyaev177/state-in-url/blob/master/assets/Demo-gif.gif)
27
34
 
35
+ # 데모
28
36
 
29
37
  <a href="https://state-in-url.dev" target="_blank">데모</a> |
30
- <a href="https://state-in-url.netlify.app/" target="_blank">DEMO2</a> |
38
+ <a href="https://state-in-url.netlify.app/" target="_blank">미러 링크</a>
31
39
 
40
+ <a href="https://github.com/asmyshlyaev177/state-in-url/blob/master/Limits.md" target="_blank">URI 크기 제한, <b>최대 12KB</b>까지 안전</a>
32
41
 
33
42
  <hr />
34
43
 
35
- 프로젝트를 지원하려면 ⭐️을 추가해주세요!
44
+ 프로젝트를 지원하려면 <a href="#"><img src="https://raw.githubusercontent.com/acervenky/animated-github-badges/master/assets/starbadge.gif" width="25" height="25"></a>를 추가하고 <a href="https://github.com/asmyshlyaev177" target="_blank">팔로우</a>해주세요!
45
+
46
+ [토론](https://github.com/asmyshlyaev177/state-in-url/discussions/1)에서 여러분의 피드백/의견을 공유해주시면 감사하겠습니다
47
+
48
+ 유용하다면 공유해주세요.
49
+ [X.com](https://twitter.com/intent/tweet?&url=https://github.com/asmyshlyaev177/state-in-url)
50
+ [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://github.com/asmyshlyaev177/state-in-url)
51
+ [FB](https://www.facebook.com/sharer.php?u=https://github.com/asmyshlyaev177/state-in-url)
52
+ [VK](http://vk.com/share.php?url=https://github.com/asmyshlyaev177/state-in-url)
36
53
 
37
54
  <hr />
38
55
 
39
56
  </div>
40
57
 
41
- ## state-in-url을 사용해야 하는 이유는?
58
+ [바로 코드 보기!](#useurlstate)
59
+
60
+ ## 왜 `state-in-url`을 사용해야 하나요?
42
61
 
43
- `state-in-url`은 URL 동기화와 함께 간단한 상태 관리를 제공합니다. 관련 없는 React 컴포넌트 간에 복잡한 상태를 공유하고, 상태를 URL과 동기화하며, TS 친화적이고 NextJS와 호환됩니다.
62
+ 쿼리 매개변수에 모든 사용자 상태를 저장하세요. 브라우저 URL에 JSON이 있다고 상상해보세요. 데이터의 타입과 구조를 유지하면서 모든 것을 저장할 수 있습니다. 예를 들어 숫자는 문자열이 아닌 숫자로, 날짜는 날짜로 디코딩되며, 객체와 배열도 지원됩니다.
63
+ 매우 간단하고 빠르며 정적 Typescript 유효성 검사를 제공합니다. 딥 링크, 즉 URL 동기화가 쉬워집니다.
44
64
 
65
+ Next.js와 react-router용 `useUrlState` 훅과 그 외 JS 환경을 위한 헬퍼를 포함합니다.
66
+ 최신 브라우저는 거대한 URL을 지원하고 사용자는 쿼리 문자열을 신경 쓰지 않습니다(전체 선택 후 복사/붙여넣기 워크플로우입니다).
45
67
 
46
- # 사용 사례
68
+ 쿼리 문자열을 원래 의도대로 상태 관리에 사용할 때입니다.
69
+ 이 라이브러리는 모든 번거로운 작업을 대신 처리합니다.
47
70
 
48
- - 🙃 URL을 변경하지 않고 다른 컴포넌트 간에 상태를 공유합니다. 시그널 및 기타 상태 관리 도구의 대안으로 좋습니다.
49
- - 🔗 전체 애플리케이션 상태가 포함된 공유 가능한 URL
50
- - 🔄 페이지 새로고침 시 상태를 쉽게 유지
51
- - 🧠 관련 없는 클라이언트 컴포넌트 간 데이터 동기화
52
- - 🧮 저장되지 않은 사용자 양식을 URL에 저장
71
+ 이 라이브러리는 NUQS의 좋은 대안입니다.
53
72
 
54
- # 특징
73
+ ### 사용 사례
55
74
 
56
- - 🧩 **간단함**: 프로바이더, 리듀서, 보일러플레이트 또는 새로운 개념이 필요 없습니다.
57
- - 📘 **TypeScript 지원 및 타입 안전성**: 데이터 타입과 구조를 보존하고, IDE 제안, 강력한 타입 지정 및 JSDoc 주석으로 개발자 경험을 향상시킵니다.
58
- - ⚛️ **프레임워크 유연성**: Next.js와 React.js 애플리케이션을 위한 별도의 훅과 순수 JS를 위한 함수 제공
59
- - ⚙ **철저한 테스트**: 단위 테스트 및 Playwright 테스트
60
- - ⚡ **빠름**: 최소한의 리렌더링
61
- - 🪶 **경량**: 작은 크기를 위해 의존성이 없습니다.
75
+ - URL에 저장되지 않은 사용자 폼이나 페이지 필터 저장
76
+ - URL과 React 상태 동기화
77
+ - URI를 건드리지 않고 관련 없는 클라이언트 컴포넌트 간 데이터 동기화
78
+ - 애플리케이션 상태가 포함된 공유 가능한 URL (딥 링킹, URL 상태 동기화)
79
+ - 페이지 새로고침 시 상태 지속성 유지
62
80
 
81
+ ### 특징
82
+
83
+ - 🧩 **간단함**: 프로바이더, 리듀서, 보일러플레이트 또는 새로운 개념 없음, `React.useState`와 유사한 API
84
+ - 📘 **Typescript 유효성 검사/자동완성**: 상태는 단순한 객체이며, Typescript 정의에 따라 IDE/테스트에서 자동 정적 유효성 검사
85
+ - ✨ **복잡한 데이터**: 중첩 객체, 날짜 및 배열, JSON과 동일하게 작동하지만 URL에서
86
+ - ☂ **기본값**: URL에 매개변수가 없으면 기본값 제공
87
+ - ⌨ **체계적**: 모든 가능한 값을 시작 시 정의하여 존재하지 alınandan 키를 가져오는 것을 방지
88
+ - **호환성**: 서드파티 쿼리 매개변수를 그대로 유지
89
+ - **유연성**: 동일한 페이지에서 1개 이상의 상태 객체 사용 가능, 다른 키만 사용하면 됨
90
+ - **빠름**: 최소한의 리렌더링, 큰 객체 인코딩 및 디코딩에 약 [1ms](https://github.com/asmyshlyaev177/state-in-url/blob/87c8c7c995c5cd7d9e7aa039c30bfe64b24abe4b/packages/urlstate/encoder/encoder.test.ts#L185) 소요
91
+ - **서버 사이드 렌더링**: 서버 컴포넌트에서 사용 가능, Next.js 14 및 15 지원
92
+ - **경량**: 의존성 없음, 라이브러리 크기 2KB 미만
93
+ - **개발자 경험**: 좋은 개발자 경험, 문서, JSDoc 주석 및 예제
94
+ - **프레임워크 유연성**: `Next.js` 및 `react-router`용 훅, 다른 프레임워크나 순수 JS와 함께 사용할 수 있는 헬퍼
95
+ - **충분한 테스트**: [Chrome/Firefox/Safari용 단위 테스트 및 Playwright 테스트](https://github.com/asmyshlyaev177/state-in-url/actions/workflows/tests.yml)
96
+ - **자유로운 라이선스**: MIT
63
97
 
64
98
  ## 목차
65
99
 
66
- - [설치](#설치)
67
- - [Next.js용 `useUrlState`](#nextjs용-useurlstate-훅)
68
- - [React.js용 `useSharedState`](#reactjs용-usesharedstate-훅)
69
- - [React.js용 `useUrlEncode`](#reactjs용-useurlencode-훅)
70
- - [순수 JS 사용을 위한 `encodeState` 및 `decodeState`](#순수-js-사용을-위한-encodestate-및-decodestate-헬퍼)
71
- - [URL과 상태 자동 동기화](#url과-상태-자동-동기화)
72
- - [저수준 `encode` 및 `decode` 함수](#저수준-encode-및-decode-함수)
73
- - [모범 사례](#모범-사례)
74
- - [주의사항](#주의사항)
75
- - [연락 및 지원](#연락-및-지원)
76
- - [변경 로그](#변경-로그)
77
- - [라이선스](#라이선스)
78
- - [영감](#영감)
100
+ - [State in url](#state-in-url)
101
+ - [데모](#데모)
102
+ - [왜 `state-in-url`을 사용해야 하나요?](#왜-state-in-url을-사용해야-하나요)
103
+ - [사용 사례](#사용-사례)
104
+ - [특징](#특징)
105
+ - [목차](#목차)
106
+ - [설치](#설치)
107
+ - [1. 패키지 설치](#1-패키지-설치)
108
+ - [2. tsconfig.json 수정](#2-tsconfigjson-수정)
109
+ - [useUrlState](#useurlstate)
110
+ - [Next.js용 useUrlState 훅](#nextjs용-useurlstate-훅)
111
+ - [사용 예제](#사용-예제)
112
+ - [기본](#기본)
113
+ - [서버 사이드 렌더링과 함께](#서버-사이드-렌더링과-함께)
114
+ - [`layout` 컴포넌트에서 훅 사용](#layout-컴포넌트에서-훅-사용)
115
+ - [임의의 상태 형태 사용 (권장하지 않음)](#임의의-상태-형태-사용-권장하지-않음)
116
+ - [Remix.js용 useUrlState 훅](#remixjs용-useurlstate-훅)
117
+ - [예제](#예제)
118
+ - [React-Router용 useUrlState 훅](#react-router용-useurlstate-훅)
119
+ - [예제](#예제-1)
120
+ - [레시피](#레시피)
121
+ - [상태의 일부를 편리하게 다루기 위한 커스텀 훅](#상태의-일부를-편리하게-다루기-위한-커스텀-훅)
122
+ - [복잡한 상태 구조 사용](#복잡한-상태-구조-사용)
123
+ - [상태만 업데이트하고 URL에 수동으로 동기화](#상태만-업데이트하고-url에-수동으로-동기화)
124
+ - [기타 훅과 헬퍼](#기타-훅과-헬퍼)
125
+ - [다른 라우터용 `useUrlStateBase` 훅](#다른-라우터용-useurlstatebase-훅)
126
+ - [React.js용 `useSharedState` 훅](#reactjs용-usesharedstate-훅)
127
+ - [React.js용 `useUrlEncode` 훅](#reactjs용-useurlencode-훅)
128
+ - [`encodeState` 및 `decodeState` 헬퍼](#encodestate-및-decodestate-헬퍼)
129
+ - [`encode` 및 `decode` 헬퍼](#encode-및-decode-헬퍼)
130
+ - [모범 사례](#모범-사례)
131
+ - [주의사항](#주의사항)
132
+ - [기타](#기타)
133
+ - [기여 및/또는 로컬 실행](#기여-및또는-로컬-실행)
134
+ - [로드맵](#로드맵)
135
+ - [문의 및 지원](#문의-및-지원)
136
+ - [변경 로그](#변경-로그)
137
+ - [언급](#언급)
138
+ - [라이선스](#라이선스)
139
+ - [영감](#영감)
79
140
 
80
141
  ## 설치
81
142
 
@@ -92,164 +153,83 @@ pnpm add state-in-url
92
153
 
93
154
  ### 2. tsconfig.json 수정
94
155
 
95
- `tsconfig.json`의 `compilerOptions`에서 `"moduleResolution": "Node16"` 또는 `"moduleResolution": "NodeNext"`로 설정하세요.
156
+ `tsconfig.json`의 `compilerOptions`에서 `"moduleResolution": "Bundler"`, 또는 `"moduleResolution": "Node16"`, 또는 `"moduleResolution": "NodeNext"`로 설정하세요.
157
+ `"module": "ES2022"` 또는 `"module": "ESNext"` 설정이 필요할 수 있습니다.
158
+
159
+ ## useUrlState
160
+
161
+ 초기 상태를 매개변수로 받아 상태 객체, URL 업데이트 콜백, 상태만 업데이트하는 콜백을 반환하는 메인 훅입니다.
162
+ 동일한 `state` 객체를 사용하는 모든 컴포넌트는 자동으로 동기화됩니다.
96
163
 
97
- ## Next.js용 useUrlState 훅
164
+ ### Next.js용 useUrlState 훅
98
165
 
99
- [문서](packages/urlstate/next/useUrlState)
166
+ [전체 API 문서](packages/urlstate/next/useUrlState)
100
167
 
101
- `useUrlState`는 Next.js 애플리케이션을 위한 사용자 정의 React 훅으로, 클라이언트 컴포넌트 간의 통신을 쉽게 만듭니다. 복잡한 상태를 공유하고 URL 검색 매개변수와 동기화할 수 있게 해주어, 페이지 새로고침 시 상태를 유지하고 URL을 통해 애플리케이션 상태를 공유할 수 있습니다.
168
+ [React-Router 예제](#react-router용-useurlstate-훅)
102
169
 
103
- ### 사용 예시
170
+ #### 사용 예제
104
171
 
105
- #### 기본
172
+ ##### 기본
106
173
 
107
- 1. 상태 형태 정의
174
+ 1. 기본값과 함께 상태 형태 정의
108
175
 
109
- ```typescript
110
- // countState.ts
111
- // 상태 형태는 상수에 저장해야 하며, 객체를 직접 전달하지 마세요
112
- export const countState: CountState = { count: 0 }
176
+ ```typescript
177
+ // userState.ts
178
+ // 기본값과 다른 값을 가진 매개변수만 URL에 포함됩니다.
179
+ export const userState: UserState = { name: '', age: 0 }
113
180
 
114
- type CountState = { count: number }
115
- ```
181
+ // `Interface`가 아닌 `Type`을 사용하세요!
182
+ type UserState = { name: string, age: number }
183
+ ```
116
184
 
117
- 2. 임포트 및 사용
185
+ 2. 가져와서 사용
118
186
 
119
187
  ```typescript
120
188
  'use client'
121
189
  import { useUrlState } from 'state-in-url/next';
122
190
 
123
- import { countState } from './countState';
191
+ import { userState } from './userState';
124
192
 
125
193
  function MyComponent() {
126
- // 서버 컴포넌트에서 searchParams 사용 시
127
- // 예: export default async function Home({ searchParams }: { searchParams: object }) {
128
- // const { state, updateState, updateUrl } = useUrlState(countState, searchParams);
129
- const { state, updateState, updateUrl } = useUrlState(countState);
130
-
131
- // 실수로 상태를 직접 변경하지 못하게 합니다. TS 필요
132
- // state.count = 2 // <- 오류
194
+ // `replace` 인자를 전달할 수 있으며, `setUrl`이 `router.push`를 사용할지 `router.replace`를 사용할지 제어합니다. 기본값 replace=true
195
+ // 서버 컴포넌트에서 `searchParams`를 전달할 수 있으며, 서버 컴포넌트에서 데이터를 가져와야 하는 경우 `useHistory: false`를 전달하세요
196
+ const { urlState, setUrl, setState } = useUrlState(userState);
133
197
 
134
198
  return (
135
199
  <div>
136
- <p>카운트: {state.count}</p>
137
-
138
- <button onClick={() => updateUrl({ count: state.count + 1 }), { replace: true }}>
139
- 증가 (URL 업데이트)
140
- </button>
141
-
142
- // React.useState와 동일한 API
143
- <button onClick={() => updateState(currState => ({...currState, count: currState.count + 1 }) )}>
144
- 증가 (로컬만)
145
- </button>
146
- <button onClick={() => updateUrl()}>
147
- 변경 사항을 URL에 동기화
148
- // 또는 동기화하지 않고 상태만 공유
149
- </button>
150
-
151
- <button onClick={() => updateUrl(state)}>
200
+ // urlState.name은 URL이 비어있으면 `userState`의 기본값을 반환합니다
201
+ <input value={urlState.name}
202
+ // React.useState와 동일한 API, 예: setUrl(currVal => currVal + 1)
203
+ onChange={(ev) => setUrl({ name: ev.target.value }) }
204
+ />
205
+ <input value={urlState.age}
206
+ onChange={(ev) => setUrl({ age: +ev.target.value }) }
207
+ />
208
+
209
+ <input value={urlState.name}
210
+ onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
211
+ // 상태를 즉시 업데이트하되 필요에 따라 URL에 동기화
212
+ onBlur={() => setUrl()}
213
+ />
214
+
215
+ <button onClick={() => setUrl((_, initial) => initial)}>
152
216
  초기화
153
217
  </button>
154
- </div>
155
- )
156
- }
157
- ```
158
-
159
- #### 복잡한 상태 형태 사용
160
-
161
- ```typescript
162
- interface UserSettings {
163
- theme: 'light' | 'dark';
164
- fontSize: number;
165
- notifications: boolean;
166
- }
167
-
168
- export const userSettings: UserSettings {
169
- theme: 'light',
170
- fontSize: 16,
171
- notifications: true,
172
- }
173
- ```
174
218
 
175
- ```typescript
176
- 'use client'
177
- import { useUrlState } from 'state-in-url/next';
178
-
179
- import { userSettings } from './userSettings';
180
-
181
- function SettingsComponent() {
182
- // `state`는 UserSettings 타입에서 추론됩니다!
183
- const { state, updateUrl } = useUrlState(userSettings);
184
-
185
- const toggleTheme = () => {
186
- updateUrl(current => ({
187
- ...current,
188
- theme: current.theme === 'light' ? 'dark' : 'light',
189
- }));
190
- };
191
-
192
- // 유휴 상태일 때 상태를 URL에 동기화
193
- const timer = React.useRef(0 as unknown as NodeJS.Timeout);
194
- React.useEffect(() => {
195
- clearTimeout(timer.current);
196
- timer.current = setTimeout(() => {
197
- // 참조가 아닌 내용으로 상태를 비교하고 새 값에 대해서만 업데이트를 실행합니다
198
- updateUrl(state);
199
- }, 500);
200
-
201
- return () => {
202
- clearTimeout(timer.current);
203
- };
204
- }, [state, updateUrl]);
205
-
206
- return (
207
- <div>
208
- <h2>사용자 설정</h2>
209
- <p>테마: {state.theme}</p>
210
- <p>글꼴 크기: {state.fontSize}px</p>
211
- <button onClick={toggleTheme}>테마 전환</button>
212
- {/* 다른 설정을 업데이트하기 위한 UI 요소 */}
213
- </div>
214
- );
215
- }
216
- ...
217
-
218
- // 다른 컴포넌트
219
- function Component() {
220
- const { state } = useUrlState(defaultSettings);
221
-
222
- return (
223
- <div>
224
- <p>알림은 {state.notifications ? '켜짐' : '꺼짐'}</p>
225
219
  </div>
226
220
  )
227
221
  }
228
222
  ```
229
223
 
230
- #### 상태 자동 동기화
224
+ ##### 서버 사이드 렌더링과 함께
231
225
 
232
- ```typescript
233
- const timer = React.useRef(0 as unknown as NodeJS.Timeout);
234
- React.useEffect(() => {
235
- clearTimeout(timer.current);
236
- timer.current = setTimeout(() => {
237
- // 참조가 아닌 내용으로 상태를 비교하고 새 값에 대해서만 업데이트를 실행합니다
238
- updateUrl(state);
239
- }, 500);
240
-
241
- return () => {
242
- clearTimeout(timer.current);
243
- };
244
- }, [state, updateUrl]);
245
- ```
246
-
247
- #### 서버 사이드 렌더링 사용
226
+ <details>
227
+ <Summary>예제</Summary>
248
228
 
249
229
  ```typescript
250
230
  export default async function Home({ searchParams }: { searchParams: object }) {
251
231
  return (
252
- <Form sp={searchParams} />
232
+ <Form searchParams={searchParams} />
253
233
  )
254
234
  }
255
235
 
@@ -259,17 +239,21 @@ import React from 'react';
259
239
  import { useUrlState } from 'state-in-url/next';
260
240
  import { form } from './form';
261
241
 
262
- const Form = ({ sp }: { sp: object }) => {
263
- const { state, updateState, updateUrl } = useUrlState(form, sp);
242
+ const Form = ({ searchParams }: { searchParams: object }) => {
243
+ const { urlState, setState, setUrl } = useUrlState(form, { searchParams });
264
244
  }
265
245
  ```
266
246
 
267
- #### `layout` 컴포넌트에서 훅 사용
247
+ </details>
268
248
 
269
- 이는 까다로운 부분입니다. Next.js의 app 라우터는 서버 사이드에서 searchParams에 접근할 수 없게 합니다. 미들웨어를 사용하는 해결책이 있지만, 이는 아름답지 않고 Next.js 업데이트 후 작동이 중단될 수 있습니다.
249
+ ##### `layout` 컴포넌트에서 훅 사용
250
+
251
+ <details>
252
+ <Summary>예제</Summary>
253
+ 이 부분은 까다로운데, app router를 사용하는 nextjs는 서버 측에서 searchParams에 접근할 수 없기 때문입니다. 미들웨어를 사용하는 해결 방법이 있지만 예쁘지 않고 nextjs 업데이트 후 작동하지 않을 수 있습니다.
270
254
 
271
255
  ```typescript
272
- // 적절한 `layout.tsc`에 추가
256
+ // 적절한 `layout.tsx`에 추가
273
257
  export const runtime = 'edge';
274
258
 
275
259
  // middleware.ts
@@ -309,7 +293,12 @@ export default async function Layout({
309
293
  }
310
294
  ```
311
295
 
312
- #### 임의의 상태 형태 사용 (권장하지 않음)
296
+ </details>
297
+
298
+ ##### 임의의 상태 형태 사용 (권장하지 않음)
299
+
300
+ <details>
301
+ <Summary>예제</Summary>
313
302
 
314
303
  ```typescript
315
304
  'use client'
@@ -318,12 +307,356 @@ import { useUrlState } from 'state-in-url/next';
318
307
  const someObj = {};
319
308
 
320
309
  function SettingsComponent() {
321
- const { state, updateUrl, updateState } = useUrlState<object>(someObj);
310
+ const { urlState, setUrl, setState } = useUrlState<object>(someObj);
322
311
  }
323
312
  ```
324
313
 
325
- ## React.js용 `useSharedState` 훅
326
- React 컴포넌트 간에 상태를 공유하기 위한 훅으로, Next.js와 Vite에서 테스트되었습니다.
314
+ </details>
315
+
316
+ ### Remix.js용 useUrlState 훅
317
+
318
+ API는 Next.js 버전과 동일하지만, [NavigateOptions](https://github.com/remix-run/react-router/blob/bc693ed9f39170bda13b9e1b282fb8e9d5925f66/packages/react-router/lib/context.ts#L99) 타입의 옵션을 전달할 수 있습니다.
319
+
320
+ [API 문서](packages/urlstate/remix/useUrlState)
321
+
322
+ #### 예제
323
+
324
+ ```typescript
325
+ export const form: Form = {
326
+ name: '',
327
+ age: undefined,
328
+ agree_to_terms: false,
329
+ tags: [],
330
+ };
331
+
332
+ type Form = {
333
+ name: string;
334
+ age?: number;
335
+ agree_to_terms: boolean;
336
+ tags: { id: string; value: { text: string; time: Date } }[];
337
+ };
338
+ ```
339
+
340
+ ```typescript
341
+ import { useUrlState } from 'state-in-url/remix';
342
+
343
+ import { form } from './form';
344
+
345
+ function TagsComponent() {
346
+ const { urlState, setUrl, setState } = useUrlState(form);
347
+
348
+ const onChangeTags = React.useCallback(
349
+ (tag: (typeof tags)[number]) => {
350
+ setUrl((curr) => ({
351
+ ...curr,
352
+ tags: curr.tags.find((t) => t.id === tag.id)
353
+ ? curr.tags.filter((t) => t.id !== tag.id)
354
+ : curr.tags.concat(tag),
355
+ }));
356
+ },
357
+ [setUrl],
358
+ );
359
+
360
+ return (
361
+ <div>
362
+ <Field text="태그">
363
+ <div className="flex flex-wrap gap-2">
364
+ {tags.map((tag) => (
365
+ <Tag
366
+ active={!!urlState.tags.find((t) => t.id === tag.id)}
367
+ text={tag.value.text}
368
+ onClick={() => onChangeTags(tag)}
369
+ key={tag.id}
370
+ />
371
+ ))}
372
+ </div>
373
+ </Field>
374
+
375
+ <input value={urlState.name}
376
+ onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
377
+ // 상태를 즉시 업데이트하되 필요에 따라 URL에 동기화
378
+ onBlur={() => setUrl()}
379
+ />
380
+ </div>
381
+ );
382
+ }
383
+
384
+ const tags = [
385
+ {
386
+ id: '1',
387
+ value: { text: 'React.js', time: new Date('2024-07-17T04:53:17.000Z') },
388
+ },
389
+ {
390
+ id: '2',
391
+ value: { text: 'Next.js', time: new Date('2024-07-18T04:53:17.000Z') },
392
+ },
393
+ {
394
+ id: '3',
395
+ value: { text: 'TailwindCSS', time: new Date('2024-07-19T04:53:17.000Z') },
396
+ },
397
+ ];
398
+ ```
399
+
400
+ [예제 코드](packages/example-remix2/app/routes/Form-for-test.tsx)
401
+
402
+ ### React-Router용 useUrlState 훅
403
+
404
+ API는 Next.js 버전과 동일하지만, [NavigateOptions](https://github.com/remix-run/react-router/blob/bc693ed9f39170bda13b9e1b282fb8e9d5925f66/packages/react-router/lib/context.ts#L99) 타입의 옵션을 전달할 수 있습니다.
405
+
406
+ [API 문서](packages/urlstate/react-router/useUrlState)
407
+
408
+ #### 예제
409
+
410
+ ```typescript
411
+ export const form: Form = {
412
+ name: '',
413
+ age: undefined,
414
+ agree_to_terms: false,
415
+ tags: [],
416
+ };
417
+
418
+ type Form = {
419
+ name: string;
420
+ age?: number;
421
+ agree_to_terms: boolean;
422
+ tags: { id: string; value: { text: string; time: Date } }[];
423
+ };
424
+ ```
425
+
426
+ ```typescript
427
+ import { useUrlState } from 'state-in-url/react-router';
428
+ // react-router v6의 경우
429
+ // import { useUrlState } from 'state-in-url/react-router6';
430
+
431
+ import { form } from './form';
432
+
433
+ function TagsComponent() {
434
+ const { urlState, setUrl, setState } = useUrlState(form);
435
+
436
+ const onChangeTags = React.useCallback(
437
+ (tag: (typeof tags)[number]) => {
438
+ setUrl((curr) => ({
439
+ ...curr,
440
+ tags: curr.tags.find((t) => t.id === tag.id)
441
+ ? curr.tags.filter((t) => t.id !== tag.id)
442
+ : curr.tags.concat(tag),
443
+ }));
444
+ },
445
+ [setUrl],
446
+ );
447
+
448
+ return (
449
+ <div>
450
+ <Field text="태그">
451
+ <div className="flex flex-wrap gap-2">
452
+ {tags.map((tag) => (
453
+ <Tag
454
+ active={!!urlState.tags.find((t) => t.id === tag.id)}
455
+ text={tag.value.text}
456
+ onClick={() => onChangeTags(tag)}
457
+ key={tag.id}
458
+ />
459
+ ))}
460
+ </div>
461
+ </Field>
462
+
463
+ <input value={urlState.name}
464
+ onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
465
+ // 상태를 즉시 업데이트하되 필요에 따라 URL에 동기화
466
+ onBlur={() => setUrl()}
467
+ />
468
+ </div>
469
+ );
470
+ }
471
+
472
+ const tags = [
473
+ {
474
+ id: '1',
475
+ value: { text: 'React.js', time: new Date('2024-07-17T04:53:17.000Z') },
476
+ },
477
+ {
478
+ id: '2',
479
+ value: { text: 'Next.js', time: new Date('2024-07-18T04:53:17.000Z') },
480
+ },
481
+ {
482
+ id: '3',
483
+ value: { text: 'TailwindCSS', time: new Date('2024-07-19T04:53:17.000Z') },
484
+ },
485
+ ];
486
+ ```
487
+
488
+ [예제 코드](packages/example-react-router6/src/Form-for-test.tsx)
489
+
490
+ ## 레시피
491
+ ##### 상태의 일부를 편리하게 다루기 위한 커스텀 훅
492
+ <details>
493
+ <Summary>예제</Summary>
494
+
495
+ ```typescript
496
+ 'use client';
497
+
498
+ import React from 'react';
499
+ import { useUrlState } from 'state-in-url/next';
500
+
501
+ const form: Form = {
502
+ name: '',
503
+ age: undefined,
504
+ agree_to_terms: false,
505
+ tags: [],
506
+ };
507
+
508
+ type Form = {
509
+ name: string;
510
+ age?: number;
511
+ agree_to_terms: boolean;
512
+ tags: {id: string; value: {text: string; time: Date } }[];
513
+ };
514
+
515
+ export const useFormState = ({ searchParams }: { searchParams?: object }) => {
516
+ const { urlState, setUrl: setUrlBase, reset } = useUrlState(form, {
517
+ searchParams,
518
+ });
519
+
520
+ // 첫 번째 네비게이션은 새로운 히스토리 항목을 추가합니다
521
+ // 이후의 모든 것은 해당 항목을 교체합니다
522
+ // 이렇게 하면 2개의 항목만 있는 히스토리를 갖게 됩니다 - ['/url', '/url?key=param']
523
+
524
+ const replace = React.useRef(false);
525
+ const setUrl = React.useCallback((
526
+ state: Parameters<typeof setUrlBase>[0],
527
+ opts?: Parameters<typeof setUrlBase>[1]
528
+ ) => {
529
+ setUrlBase(state, { replace: replace.current, ...opts });
530
+ replace.current = true;
531
+ }, [setUrlBase]);
532
+
533
+ return { urlState, setUrl, resetUrl: reset };
534
+ };
535
+ ```
536
+ </details>
537
+
538
+ <hr />
539
+
540
+ ##### 복잡한 상태 구조 사용
541
+
542
+ <details>
543
+ <Summary>예제</Summary>
544
+
545
+ ```typescript
546
+ export const form: Form = {
547
+ name: '',
548
+ age: undefined,
549
+ agree_to_terms: false,
550
+ tags: [],
551
+ };
552
+
553
+ type Form = {
554
+ name: string;
555
+ age?: number;
556
+ agree_to_terms: boolean;
557
+ tags: { id: string; value: { text: string; time: Date } }[];
558
+ };
559
+ ```
560
+
561
+ ```typescript
562
+ 'use client'
563
+ import { useUrlState } from 'state-in-url/next';
564
+
565
+ import { form } from './form';
566
+
567
+ function TagsComponent() {
568
+ // `urlState`는 Form 타입에서 추론됩니다!
569
+ const { urlState, setUrl } = useUrlState(form);
570
+
571
+ const onChangeTags = React.useCallback(
572
+ (tag: (typeof tags)[number]) => {
573
+ setUrl((curr) => ({
574
+ ...curr,
575
+ tags: curr.tags.find((t) => t.id === tag.id)
576
+ ? curr.tags.filter((t) => t.id !== tag.id)
577
+ : curr.tags.concat(tag),
578
+ }));
579
+ },
580
+ [setUrl],
581
+ );
582
+
583
+ return (
584
+ <div>
585
+ <Field text="태그">
586
+ <div className="flex flex-wrap gap-2">
587
+ {tags.map((tag) => (
588
+ <Tag
589
+ active={!!urlState.tags.find((t) => t.id === tag.id)}
590
+ text={tag.value.text}
591
+ onClick={() => onChangeTags(tag)}
592
+ key={tag.id}
593
+ />
594
+ ))}
595
+ </div>
596
+ </Field>
597
+ </div>
598
+ );
599
+ }
600
+
601
+ const tags = [
602
+ {
603
+ id: '1',
604
+ value: { text: 'React.js', time: new Date('2024-07-17T04:53:17.000Z') },
605
+ },
606
+ {
607
+ id: '2',
608
+ value: { text: 'Next.js', time: new Date('2024-07-18T04:53:17.000Z') },
609
+ },
610
+ {
611
+ id: '3',
612
+ value: { text: 'TailwindCSS', time: new Date('2024-07-19T04:53:17.000Z') },
613
+ },
614
+ ];
615
+ ```
616
+
617
+ [데모 페이지 예제 코드](https://github.com/asmyshlyaev177/state-in-url/blob/master/packages/example-nextjs14/src/app/Form.tsx)
618
+ </details>
619
+
620
+ ##### 상태만 업데이트하고 URL에 수동으로 동기화
621
+
622
+ <details>
623
+ <Summary>예제</Summary>
624
+
625
+ ```typescript
626
+
627
+ const timer = React.useRef(0 as unknown as NodeJS.Timeout);
628
+ React.useEffect(() => {
629
+ clearTimeout(timer.current);
630
+ timer.current = setTimeout(() => {
631
+ // 참조가 아닌 내용으로 상태를 비교하고 새 값에 대해서만 업데이트 실행
632
+ setUrl(urlState);
633
+ }, 500);
634
+
635
+ return () => {
636
+ clearTimeout(timer.current);
637
+ };
638
+ }, [urlState, setUrl]);
639
+ ```
640
+
641
+ `onBlur`에서 상태를 동기화하는 것이 실제 사용과 더 일치합니다.
642
+
643
+ ```typescript
644
+ <input onBlur={() => updateUrl()} .../>
645
+ ```
646
+
647
+ </details>
648
+
649
+ ## 기타 훅과 헬퍼
650
+
651
+ ### 다른 라우터용 `useUrlStateBase` 훅
652
+
653
+ 다른 라우터(예: react-router 또는 tanstack router)와 함께 자체 `useUrlState` 훅을 만들기 위한 훅입니다.
654
+
655
+ [API 문서](packages/urlstate/useUrlStateBase)
656
+
657
+ ### React.js용 `useSharedState` 훅
658
+
659
+ 모든 React 컴포넌트 간에 상태를 공유하는 훅으로, Next.js 및 Vite에서 테스트되었습니다.
327
660
 
328
661
  ```typescript
329
662
  'use client'
@@ -336,59 +669,100 @@ function SettingsComponent() {
336
669
  }
337
670
  ```
338
671
 
339
- [문서](packages/urlstate/useSharedState/README.md)
672
+ [API 문서](packages/urlstate/useSharedState/README.md)
340
673
 
341
- ## React.js용 `useUrlEncode` 훅
674
+ ### React.js용 `useUrlEncode` 훅
342
675
 
343
- [문서](packages/urlstate/useUrlEncode/README.md)
676
+ [API 문서](packages/urlstate/useUrlEncode/README.md)
344
677
 
345
- ## `encodeState` 및 `decodeState` 헬퍼
678
+ ### `encodeState` 및 `decodeState` 헬퍼
346
679
 
347
- [문서](packages/urlstate/encodeState/README.md)
680
+ [API 문서](packages/urlstate/encodeState/README.md)
348
681
 
349
- ## `encode` 및 `decode` 헬퍼
682
+ ### `encode` 및 `decode` 헬퍼
350
683
 
351
- [문서](packages/urlstate/encoder/README.md)
684
+ [API 문서](packages/urlstate/encoder/README.md)
352
685
 
353
686
  ## 모범 사례
354
687
 
355
- - 일관성을 위해 상태 형태를 상수로 정의하세요
356
- - 향상된 타입 안전성과 자동 완성을 위해 TypeScript를 사용하세요
357
- - URL 매개변수에 민감한 정보를 저장하지 마세요
358
- - 빈번한 업데이트에는 `updateState`를, URL에 변경 사항을 동기화하려면 `updateUrl`을 사용하세요
359
- - Next.js에서 클라이언트 컴포넌트를 감싸려면 `Suspense`를 사용하세요
688
+ - 상태 형태를 상수로 정의하세요
689
+ - 향상된 타입 안전성과 자동완성을 위해 TypeScript를 사용하세요
690
+ - URL 매개변수에 민감한 정보(SSN, API 키 등)를 저장하지 마세요
360
691
  - 읽기 쉬운 TS 오류를 위해 이 [확장 프로그램](https://marketplace.visualstudio.com/items?itemName=yoavbls.pretty-ts-errors)을 사용하세요
361
692
 
693
+ 상태의 일부에 대한 상태 훅을 만들고 애플리케이션 전체에서 재사용할 수 있습니다. 예를 들어:
694
+ ```Typescript
695
+ type UserState = {
696
+ name: string;
697
+ age: number;
698
+ other: { id: string, value: number }[]
699
+ };
700
+ const userState = {
701
+ name: '',
702
+ age: 0,
703
+ other: [],
704
+ };
705
+
706
+ export const useUserState = () => {
707
+ const { urlState, setUrl, reset } = useUrlState(userState);
708
+
709
+ // 기타 로직
710
+
711
+ // 다른 페이지로 이동할 때 쿼리 매개변수 초기화
712
+ React.useEffect(() => {
713
+ return reset
714
+ }, [])
715
+
716
+ return { userState: urlState, setUserState: setUrl };;
717
+ }
718
+ ```
719
+
362
720
  ## 주의사항
363
721
 
364
- 1. 직렬화 가능한 값만 전달할 수 있습니다. `Function`, `BigInt` 또는 `Symbol`은 작동하지 않으며, `ArrayBuffer`와 같은 것들도 마찬가지일 것입니다.
365
- 2. Vercel 서버는 헤더(쿼리 문자열 및 기타 항목)의 크기를 **14KB**로 제한하므로 URL 상태를 ~5000 단어 이하로 유지하세요. https://vercel.com/docs/errors/URL_TOO_LONG
366
- 3. app 라우터가 있는 `next.js` 14에서 테스트되었으며, pages를 지원할 계획은 없습니다.
722
+ 1. 직렬화 가능한 값만 전달할 수 있습니다. `Function`, `BigInt` 또는 `Symbol`은 작동하지 않으며, `ArrayBuffer`와 같은 것도 마찬가지일 것입니다. JSON으로 직렬화할 수 있는 모든 것은 작동합니다.
723
+ 2. Vercel 서버는 헤더(쿼리 문자열 및 기타 항목) 크기를 **14KB**로 제한하므로 URL 상태를 약 5000단어 미만으로 유지하세요. <https://vercel.com/docs/errors/URL_TOO_LONG>
724
+ 3. app router를 사용하는 `next.js` 14/15로 테스트되었으며, pages를 지원할 계획은 없습니다.
367
725
 
368
- ## 로컬에서 실행
726
+ ## 기타
369
727
 
370
- 이 저장소를 복제하고 `npm install`을 실행한 다음
728
+ ### 기여 및/또는 로컬 실행
371
729
 
372
- ```sh
373
- npm run dev
374
- ```
730
+ [기여 문서](CONTRIBUTING.md)를 참조하세요
375
731
 
376
- [localhost:3000](http://localhost:3000)으로 이동하세요
732
+ ## 로드맵
377
733
 
734
+ - [x] `Next.js`용 훅
735
+ - [x] `react-router`용 훅
736
+ - [x] `remix`용 훅
737
+ - [ ] `svelte`용 훅
738
+ - [ ] `astro`용 훅
739
+ - [ ] 해시에 상태를 저장하는 훅?
378
740
 
379
- ## 연락 및 지원
741
+ ## 문의 및 지원
380
742
 
381
- - 버그 리포트, 기능 요청 또는 질문을 위해 [GitHub 이슈](https://github.com/asmyshlyaev177/state-in-url/issues)를 생성하세요
743
+ - 버그 보고, 기능 요청 또는 질문을 위해 [GitHub 이슈](https://github.com/asmyshlyaev177/state-in-url/issues)를 생성하세요
382
744
 
383
- ## [변경 로그](https://github.com/asmyshlyaev177/state-in-url/blob/master/CHANGELOG.md)
745
+ ## [변경 로그](CHANGELOG.md)
746
+
747
+ ## 언급
748
+
749
+ - [This Week in React 209](https://thisweekinreact.com/newsletter/209)
750
+ - [JavaScript Weekly](https://javascriptweekly.com/issues/741)
751
+ - [This Week in React 240](https://thisweekinreact.com/newsletter/240)
384
752
 
385
753
  ## 라이선스
386
754
 
387
- 이 프로젝트는 [MIT 라이선스](https://github.com/asmyshlyaev177/state-in-url/blob/master/LICENSE)에 따라 라이선스가 부여됩니다.
755
+ 이 프로젝트는 [MIT 라이선스](LICENSE)에 따라 라이선스가 부여됩니다.
756
+
757
+ ## 개인 웹사이트
758
+
759
+ [포트폴리오 사이트](https://asmyshlyaev177.dev)
388
760
 
389
761
  ## 영감
390
762
 
391
- [Vue에서 URL을 사용하여 상태 저장하기](https://dev.to/jacobandrewsky/using-url-to-store-state-in-vue-275c)
763
+ [NUQS](https://github.com/47ng/nuqs)
764
+
765
+ [Vue에서 URL을 사용하여 상태 저장](https://dev.to/jacobandrewsky/using-url-to-store-state-in-vue-275c)
392
766
 
393
767
  [URL에 상태 저장하기](https://antonz.org/storing-state/)
394
768