์๋ฌธ
https://tkdodo.eu/blog/deriving-client-state-from-server-state
Deriving Client State from Server State
How to use derived state in React to keep client state and server data aligned without manual sync or effects.
tkdodo.eu
ํด๊ฐ์์ ๋ง ๋์์๋๋ฐ, Zustand๋ฅผ ์ฌ์ฉํ ๋ ๊ฐ์ฅ ํฐ ํธ๋ ์ด๋์คํ์ ๋ํ Reddit ์ง๋ฌธ์ ๋ณด๊ฒ ๋์๋ค.
์ฝ๋๋ ๋๋ต ์ด๋ ๋ค(์ต์ ๋ฌธ๋ฒ์ผ๋ก ์กฐ๊ธ ์์ ํ๊ณ , ์ปค์คํ
ํ
์์ ๋ฃ์๋ค).
Manual-sync
const useSelectedUser = () => {
const { data: users } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
})
const { selectedUserId, setSelectedUserId } = useUserStore()
// If the selected user gets deleted from the server,
// Zustand won't automatically clear selectedUserId
// You have to manually handle this:
useEffect(() => {
if (!users?.some((u) => u.id === selectedUserId)) {
setSelectedUserId(null) // Manual sync required
}
}, [users, selectedUserId])
return [selectedUserId, selectedUserId]
}
๋ฌผ๋ก , useEffect—ํนํ ๊ทธ ์์์ setState๋ฅผ ํธ์ถํ๋ ๊ฒฝ์ฐ—๋ฅผ ๋ณผ ๋๋ง๋ค ๋๋ ๋ ๋์ ํด๊ฒฐ์ฑ
์ด ์์๊น ์๊ฐํ๊ฒ ๋๋ค.
๋ด ๊ฒฝํ์, ๊ฑฐ์ ํญ์ ๋ ๋์ ๋ฐฉ๋ฒ์ด ์กด์ฌํ๋ฉฐ, ๋ณดํต ๊ทธ ๋ฐฉ๋ฒ์ ์ฐพ๋ ๊ฒ์ด ๊ฐ์น ์๋ ์ผ์ด๋ค.
๊ทธ๋ฌ๋ ํ ๊ฑธ์ ๋ฌผ๋ฌ์์ ์ฐ๋ฆฌ๊ฐ ๋จผ์ ๋ฌด์์ ๋ฌ์ฑํ๊ณ ์ถ์์ง๋ฅผ ๋จผ์ ์ดํด๋ณด์.
์ํ ๋๊ธฐํ ์ ์งํ๊ธฐ (Keeping State in Sync)
๊ฐ๋จํ ๋งํ๋ฉด, ์ฐ๋ฆฌ๋ ํด๋ผ์ด์ธํธ ์ํ(Client State)์ธ selectedUserId๋ฅผ ์๋ฒ ์ํ(Server State)์ธ ์ฌ์ฉ์ ๋ชฉ๋ก(users)๊ณผ ๋๊ธฐํํ๊ณ ์ถ๋ค.
์ด๊ฒ์ ๋น์ฐํ ์ผ์ด๋ค.
์๋ฅผ ๋ค์ด, useQuery๋ฅผ ํตํด ๋ฐฑ๊ทธ๋ผ์ด๋์์ ๋ค์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์๋๋ฐ, ์ฌ์ฉ์๊ฐ ๋ชฉ๋ก์์ ์ญ์ ๋์์์๋ ์ฌ์ ํ ์ํ์ ์ ์ฅ๋์ด ์๋ค๋ฉด, ๊ทธ ์ ํ์ ์ ํจํ์ง ์๋ค.
ํด๋ผ์ด์ธํธ ์ํ(Client State)
์ด ์ ๊ทผ ๋ฐฉ์์ Zustand์๋ง ๊ตญํ๋ ๊ฒ์ด ์๋๋ค.
useUserStore()๋ฅผ ๋จ์ํ ๋ก์ปฌ ์ํ๋ก ๋ฐ๊ฟ๋ ๋๊ฐ์ด ์ ์ฉํ ์ ์๋ค:
const [selectedUserId, setSelectedUserId] = useState()
๋๋ URL ์ํ(nuqs ๊ฐ์)๋ฅผ ์ฌ์ฉํด๋ ๋๋ค:
const [selectedUserId, setSelectedUserId] = useQueryState('userId')
์ค์ํ ๊ฑด ์ํ๊ฐ ์ด๋์ ์ ์ฅ๋๋๋๊ฐ ์๋๋ผ,
์๋ฒ ์ํ(Server State)๊ฐ ๋ฐ๋ ๋ ์
๋ฐ์ดํธํด์ผ ํ๋ ํด๋ผ์ด์ธํธ ์ํ(Client State)๋ผ๋ ์ ์ด๋ค.
์ฟผ๋ฆฌ(Query)์๋ onSuccess ์ฝ๋ฐฑ์ด ์๊ณ ,
๋ ๋ ์ค์ setState๋ฅผ ํธ์ถํ๋ ํธ๋ฆญ์ React์ ๋ด์ฅ ์ํ์์๋ง ๋์ํ๊ธฐ ๋๋ฌธ์,
์ฌ์ค์ ๋จ์ ์ ์ผํ ๋ฐฉ๋ฒ์ ๋ฌด์๋ฌด์ํ useEffect๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด๋ค.
๊ฒฐ๊ตญ user selection์ ์ ๋ฐ์ดํธํ๋ ค๋ฉด ๋ค๋ฅธ ๋ฐฉ๋ฒ์ด ์์๊น?
์ํ๋ฅผ ๋๊ธฐํํ์ง ๋ง๊ณ , ํ์ํ๋ผ(Don’t Sync State – Derive It)
Kent C. Dodds๊ฐ ์ด ๊ธ์ ๊ธฐ์ตํ๋๊ฐ?
๊ทธ๋ ๋ค ๊ฐ์ ์๋ก ๋ค๋ฅธ useState๋ฅผ ํ๋๋ก ์ค์ด๊ณ , ๋๋จธ์ง๋ ๋จ์ผ ์ง์ค์ ์ถ์ฒ(source of truth)์์ ํ์ํ๋ ๋ฐฉ๋ฒ์ ๋ณด์ฌ์ฃผ์๋ค.
์ฐ๋ฆฌ ์ํฉ์์๋ ๋น์ทํ ์ ๊ทผ์ ํ ์ ์๋ค.
useEffect๋ฅผ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ ๋ค์ ๋ช
๋ นํ(imperative) ์ฌ๊ณ ๋ฐฉ์์ด๋ค:
์ฌ์ฉ์๊ฐ ๋ณ๊ฒฝ๋๊ณ , ํ์ฌ ์ ํ์ด ์ ํจํ์ง ์์ผ๋ฉด, ์ ํ์ null๋ก ์ฌ์ค์ ํ๋ค.
ํ์ง๋ง ์ด ์ฌ๊ณ ๋ฐฉ์์ ์กฐ๊ธ ๋ ์ ์ธํ(declarative)์ผ๋ก ๋ฐ๊ฟ ์ ์๋ค:
์ฌ๊ธฐ ๋ฐฑ์๋์์ ๊ฐ์ ธ์จ Users์ Current selection์ด ์๋ค.
์ด ์ ๋ณด๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์ค์ ์ํ(real state)๋ฅผ ๋ฌ๋ผ.
const useSelectedUser = () => {
const { data: users } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
})
const { selectedUserId, setSelectedUserId } = useUserStore()
const selectedId = users?.some((u) => u.id === selectedUserId)
? selectedUserId
: null
return [selectedId, setSelectedUserId]
}
์ด ์ฝ๋๋ ๋งค์ฐ ๊ฐ๋จํ๋ค.
์คํ ์ด ๊ฐ์ ์ง์ ์
๋ฐ์ดํธํ๋ ๋์ , Selection ์์ฒด๋ ๊ทธ๋๋ก ๋๊ณ ,
์๋ฒ ์ํ(Server State)์์ ๋ ์ด์ ํด๋น id๋ฅผ ์ฐพ์ ์ ์๋ ๊ฒฝ์ฐ ์ปค์คํ
ํ
์์ ๋ค๋ฅธ ๊ฐ์ ๋ฐํํ๋๋ก ํ๋ค.
์ฆ, useSelectedUser()๋ฅผ ํธ์ถํ๋ ๊ณณ์์๋ ์ด์ ์ฒ๋ผ null์ ๋ฐ๊ฒ ๋๋ค.
๊ทธ๋ฆฌ๊ณ ์คํ ์ด๋ฅผ ๊ฑด๋๋ฆฌ์ง ์๊ธฐ ๋๋ฌธ์ ์ถ๊ฐ์ ์ธ ์ด์ ๋ ์๋ค:
- ์ฌ์ฉ์๊ฐ ๋ค์ ๋ชฉ๋ก์ ์ถ๊ฐ๋๋ฉด, ์ ํ(selection)์ด ์๋์ผ๋ก ๋ณต์๋๋ค.
- UX ์๊ตฌ์ฌํญ์ด ๋ฐ๋์ด ์ ํ์ ์ ๊ฑฐํ๊ณ ์ถ์ง ์๊ณ , ๋จ์ง ์ ํ์ด ์ ํจํ์ง ์์์ ์๊ฐ์ ์ผ๋ก ํ์ํ๊ณ ์ถ์ ๋๋ ๊ฐ๋ฅํ๋ค.
→ ์๋ ๊ฐ์ ํญ์ ์ ์งํ๊ธฐ ๋๋ฌธ์ ์ฝ๊ฒ ๊ตฌํํ ์ ์๋ค.
isSelectionValid
const useSelectedUser = () => {
const { data: users } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
})
const { selectedUserId, setSelectedUserId } = useUserStore()
const isSelectionValid = users?.some((u) => u.id === selectedUserId)
return [selectedUserId, setSelectedUserId, isSelectionValid]
}
ํจ์ ์ ์ด๋์ ์์๊น์?
"์ํ ํ์ ์๋ฃจ์
"์ ๋ช
๋ฐฑํ ๋จ์ ์ค ํ๋๋ ์ฌ์ฉ์ ์ ์ฅ์์ ์ ์ฅ๋ ๋ด์ฉ์ ๋ ์ด์ "์ ๋ขฐ"ํ ์ ์๋ค๋ ๊ฒ์
๋๋ค. useUserStore์์ ๋ค๋ฅธ ๊ณณ์ selectedUserId๋ฅผ ์ฝ์ด์ค๋ฉด ์ถ๊ฐ ๊ฒ์ฌ๋ฅผ ๋ฐ์ง ๋ชปํ๋ฏ๋ก ํญ์ ์ฌ์ฉ์ ์ ์ hook์์ ์ฝ์ด์ผ ํฉ๋๋ค.
์ ๋ ์ ์ฅ์๋ฅผ ์ต์ข
๊ฒ์ฆ๋ ๊ฐ์ ์์ค๋ผ๊ธฐ๋ณด๋ค๋ UI์์ ์ค์ ๋ก ์ ํ๋ ํญ๋ชฉ์ ๊ธฐ๋ก์ผ๋ก ๋ณด๊ธฐ ๋๋ฌธ์ ์ด ์ ์ ๋ํด์๋ ์ ํ ๋ฌธ์ ๊ฐ ์์ต๋๋ค.
๋, Reddit ์ง๋ฌธ์์ Redux Toolkit์ด “์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ค”๊ณ ์ธ๊ธํ์ง๋ง, ์ค์ ๋ก ํฌ๊ฒ ๋ค๋ฅด์ง ์์ ๊ฒ์ด๋ค.
๋ณดํต API ์ฌ๋ผ์ด์ค์ ์ฌ์ฉ์ ์ ํ ์ฌ๋ผ์ด์ค๋ฅผ ์ฝ์ด ๋ ๊ฐ์ ๊ฒฐํฉํ๋ selector๋ฅผ ์์ฑํ๊ฒ ๋๋๋ฐ,
์ด๊ฒ์ด ๋ฐ๋ก ์ฐ๋ฆฌ๊ฐ ์ปค์คํ
ํ
์์ ํ๊ณ ์๋ ๋ฐฉ์๊ณผ ๋์ผํ๋ค.
์คํ๋ ค ์ด ๋ฐฉ์์ ์ํ๋ฅผ ํ์(derive)ํ๋๋ก ์ ๋ํ๋ฏ๋ก, ์ข์ ์ ๊ทผ์ ๋๋ค.
๋ค๋ฅธ ์์
์๋ฒ ์ํ(Server State)๊ฐ ๋ฐ๋์ด๋ ํด๋ผ์ด์ธํธ ์ํ(Client State)๋ฅผ ์
๋ฐ์ดํธํ์ง ์๋ ๊ฐ๋
์ ์ฌ๋ฌ ๊ฒฝ์ฐ์ ์ ์ฉํ๋ค.
ํํ ์์๋ ์๋ฒ์์ ๊ฐ์ ธ์จ ๊ธฐ๋ณธ๊ฐ์ผ๋ก ํผ์ ๋ฏธ๋ฆฌ ์ฑ์ธ ๋(prefilling forms)์ด๋ค.
default-value-effect
function UserSelection() {
const { data: users } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
})
const [selection, setSelection] = useState()
// use the first value as default selection
useEffect(() => {
if (users?.[0]) {
setSelection(users[0])
}
}, [users])
// return markup
}
์ด ํจ๊ณผ๋ ์ฅํฉํ ๋ฟ๋ง ์๋๋ผ, ์ฟผ๋ฆฌ์์ ์ ๋ฐ์ดํฐ๊ฐ ๋ค์ด์ฌ ๋ ํ์ฌ ์ ํ ํญ๋ชฉ์ ๋ฎ์ด์ฐ๋ ๋ฒ๊ทธ๋ ์์ต๋๋ค. ๐
์ด ๋ฌธ์ ๋ ๋ค๋ฅธ ๊ฒ์ฌ๋ฅผ ์ถ๊ฐํ๋ฉด ์ฝ๊ฒ ํด๊ฒฐํ ์ ์์ง๋ง, ๋ ๋์ ํด๊ฒฐ์ฑ ์ ์ฌ์ ํ โโ์ํ๋ฅผ ํ์ํ๋ ๊ฒ์ ๋๋ค.
derived-default-value
function UserSelection() {
const { data: users } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
})
const [selection, setSelection] = useState()
const derivedSelection = selection ?? users?.[0]
// return markup
}
์ง๊ธ ์ฐ๋ฆฌ๊ฐ ํด์ผ ํ ์ผ์ selection ๋์ derivedSelection์ ๊ณ์ ์ฌ์ฉํ๋ ๊ฒ๋ฟ์ด๋ฉฐ, ๊ทธ๋ ๊ฒ ํ๋ฉด ํญ์ ์ํ๋ ๊ฐ์ ์ป์ ์ ์์ต๋๋ค. ๐
'Developing๐ฉโ๐ป > Front-end' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| [Next.js] SSG, CSR, SSR, ISR ๋ด๋ถ ์๋์๋ฆฌ(feat. Next.js) (0) | 2025.10.10 |
|---|---|
| [์๋ฌธ ๋ฒ์ญ] useCallback์ ์ธ๋ชจ์์ - tkdodo (0) | 2025.10.07 |
| [Next.js] nuqs์ useQueryState (0) | 2025.10.06 |
| [FE] ๋น๋๊ธฐ ๋์ ๋์์ฑ์ ๊ตฌํํ๋ Web Worker, WebAssembly ์์๋ณด๊ธฐ(๋ฉํฐ์ค๋ ๋) (0) | 2025.08.24 |
| [FE] Webpack, Rollup.js, ๊ทธ๋ฆฌ๊ณ Vite (0) | 2025.08.22 |
