state-in-url 6.0.3 → 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.CN.md +366 -254
- package/README.KO.md +575 -201
- package/package.json +1 -1
- package/dist/encoder/encoder.test.d.ts +0 -1
- package/dist/parseSPObj.test.d.ts +0 -1
- package/dist/useSharedState/useSharedState.test.d.ts +0 -14
- package/dist/useUrlEncode/useUrlEncode.test.d.ts +0 -1
- package/dist/useUrlStateBase/useUrlStateBase.test.d.ts +0 -1
- package/dist/utils.test.d.ts +0 -1
- /package/dist/{encodeState/encodeState.test.d.ts → exportsTest.d.ts} +0 -0
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="/
|
|
4
|
+
<img src="/packages/example-nextjs14/public/Logo_symbol.png" alt="state-in-url 로고" width="120px" />
|
|
6
5
|
|
|
7
|
-
|
|
6
|
+
# State in url
|
|
8
7
|
</div>
|
|
9
8
|
|
|
10
|
-
|
|
11
9
|
<div align="center">
|
|
12
|
-
|
|
10
|
+
</div>
|
|
13
11
|
|
|
12
|
+
<div align="center">
|
|
14
13
|
|
|
15
14
|
[](https://www.npmjs.com/package/state-in-url)
|
|
16
|
-

|
|
17
16
|
[](https://app.codacy.com/gh/asmyshlyaev177/state-in-url/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
|
|
17
|
+
|
|
18
|
+

|
|
19
|
+
[](https://app.codacy.com/gh/asmyshlyaev177/state-in-url/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_coverage)
|
|
18
20
|
[](https://github.com/asmyshlyaev177/state-in-url/)
|
|
19
|
-
]([https://github.com/semantic-release/semantic-release](https://github.com/asmyshlyaev177/state-in-url))
|
|
22
|
+
|
|
23
|
+
[](https://scorecard.dev/viewer/?uri=github.com/asmyshlyaev177/state-in-url)
|
|
24
|
+
[](https://www.bestpractices.dev/projects/9679)
|
|
25
|
+
[](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">버그를
|
|
31
|
+
<h4 align="center">버그를 발견하거나 기능 요청이 있으시면 주저하지 말고 이슈를 열어주세요</h4>
|
|
25
32
|
|
|
26
|
-

|
|
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"
|
|
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
|
-
|
|
58
|
+
[바로 코드 보기!](#useurlstate)
|
|
59
|
+
|
|
60
|
+
## 왜 `state-in-url`을 사용해야 하나요?
|
|
42
61
|
|
|
43
|
-
|
|
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
|
-
|
|
49
|
-
- 🔗 전체 애플리케이션 상태가 포함된 공유 가능한 URL
|
|
50
|
-
- 🔄 페이지 새로고침 시 상태를 쉽게 유지
|
|
51
|
-
- 🧠 관련 없는 클라이언트 컴포넌트 간 데이터 동기화
|
|
52
|
-
- 🧮 저장되지 않은 사용자 양식을 URL에 저장
|
|
71
|
+
이 라이브러리는 NUQS의 좋은 대안입니다.
|
|
53
72
|
|
|
54
|
-
|
|
73
|
+
### 사용 사례
|
|
55
74
|
|
|
56
|
-
-
|
|
57
|
-
-
|
|
58
|
-
-
|
|
59
|
-
-
|
|
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
|
-
- [
|
|
68
|
-
- [
|
|
69
|
-
- [
|
|
70
|
-
- [
|
|
71
|
-
- [
|
|
72
|
-
- [
|
|
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": "
|
|
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
|
-
|
|
164
|
+
### Next.js용 useUrlState 훅
|
|
98
165
|
|
|
99
|
-
[문서](packages/urlstate/next/useUrlState)
|
|
166
|
+
[전체 API 문서](packages/urlstate/next/useUrlState)
|
|
100
167
|
|
|
101
|
-
|
|
168
|
+
[React-Router 예제](#react-router용-useurlstate-훅)
|
|
102
169
|
|
|
103
|
-
|
|
170
|
+
#### 사용 예제
|
|
104
171
|
|
|
105
|
-
|
|
172
|
+
##### 기본
|
|
106
173
|
|
|
107
|
-
1. 상태 형태 정의
|
|
174
|
+
1. 기본값과 함께 상태 형태 정의
|
|
108
175
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
176
|
+
```typescript
|
|
177
|
+
// userState.ts
|
|
178
|
+
// 기본값과 다른 값을 가진 매개변수만 URL에 포함됩니다.
|
|
179
|
+
export const userState: UserState = { name: '', age: 0 }
|
|
113
180
|
|
|
114
|
-
|
|
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 {
|
|
191
|
+
import { userState } from './userState';
|
|
124
192
|
|
|
125
193
|
function MyComponent() {
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
<button onClick={() =>
|
|
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
|
-
|
|
233
|
-
|
|
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
|
|
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 = ({
|
|
263
|
-
const {
|
|
242
|
+
const Form = ({ searchParams }: { searchParams: object }) => {
|
|
243
|
+
const { urlState, setState, setUrl } = useUrlState(form, { searchParams });
|
|
264
244
|
}
|
|
265
245
|
```
|
|
266
246
|
|
|
267
|
-
|
|
247
|
+
</details>
|
|
268
248
|
|
|
269
|
-
|
|
249
|
+
##### `layout` 컴포넌트에서 훅 사용
|
|
250
|
+
|
|
251
|
+
<details>
|
|
252
|
+
<Summary>예제</Summary>
|
|
253
|
+
이 부분은 까다로운데, app router를 사용하는 nextjs는 서버 측에서 searchParams에 접근할 수 없기 때문입니다. 미들웨어를 사용하는 해결 방법이 있지만 예쁘지 않고 nextjs 업데이트 후 작동하지 않을 수 있습니다.
|
|
270
254
|
|
|
271
255
|
```typescript
|
|
272
|
-
// 적절한 `layout.
|
|
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 {
|
|
310
|
+
const { urlState, setUrl, setState } = useUrlState<object>(someObj);
|
|
322
311
|
}
|
|
323
312
|
```
|
|
324
313
|
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
674
|
+
### React.js용 `useUrlEncode` 훅
|
|
342
675
|
|
|
343
|
-
[문서](packages/urlstate/useUrlEncode/README.md)
|
|
676
|
+
[API 문서](packages/urlstate/useUrlEncode/README.md)
|
|
344
677
|
|
|
345
|
-
|
|
678
|
+
### `encodeState` 및 `decodeState` 헬퍼
|
|
346
679
|
|
|
347
|
-
[문서](packages/urlstate/encodeState/README.md)
|
|
680
|
+
[API 문서](packages/urlstate/encodeState/README.md)
|
|
348
681
|
|
|
349
|
-
|
|
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
|
-
- 향상된 타입 안전성과
|
|
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 서버는 헤더(쿼리 문자열 및 기타 항목)
|
|
366
|
-
3. app
|
|
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
|
-
|
|
728
|
+
### 기여 및/또는 로컬 실행
|
|
371
729
|
|
|
372
|
-
|
|
373
|
-
npm run dev
|
|
374
|
-
```
|
|
730
|
+
[기여 문서](CONTRIBUTING.md)를 참조하세요
|
|
375
731
|
|
|
376
|
-
|
|
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
|
-
- 버그
|
|
743
|
+
- 버그 보고, 기능 요청 또는 질문을 위해 [GitHub 이슈](https://github.com/asmyshlyaev177/state-in-url/issues)를 생성하세요
|
|
382
744
|
|
|
383
|
-
## [변경 로그](
|
|
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 라이선스](
|
|
755
|
+
이 프로젝트는 [MIT 라이선스](LICENSE)에 따라 라이선스가 부여됩니다.
|
|
756
|
+
|
|
757
|
+
## 개인 웹사이트
|
|
758
|
+
|
|
759
|
+
[포트폴리오 사이트](https://asmyshlyaev177.dev)
|
|
388
760
|
|
|
389
761
|
## 영감
|
|
390
762
|
|
|
391
|
-
[
|
|
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
|
|