์›๋ฌธ

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์„ ๊ณ„์† ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ๋ฟ์ด๋ฉฐ, ๊ทธ๋ ‡๊ฒŒ ํ•˜๋ฉด ํ•ญ์ƒ ์›ํ•˜๋Š” ๊ฐ’์„ ์–ป์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๐Ÿš€

 

 

https://tkdodo.eu/blog/the-useless-use-callback

 

The Useless useCallback

Why most memoization is downright useless...

tkdodo.eu

์›๋ฌธ

 

 

๋ฉ”๋ชจ์ด์ œ์ด์…˜(memoization)์— ๋Œ€ํ•ด์„œ๋Š” ์ด์ œ ์ถฉ๋ถ„ํžˆ ๋‹ค๋ค˜๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๋Š”๋ฐ, ์š”์ฆ˜ ์ž์ฃผ ๋ณด์ด๋Š” ํ•œ ๊ฐ€์ง€ ํŒจํ„ด์„ ๋ณด๋ฉด์„œ ์ƒ๊ฐ์ด ๋‹ฌ๋ผ์กŒ๋‹ค.
๊ทธ๋ž˜์„œ ์˜ค๋Š˜์€ useCallback๊ณผ, ๋ถ€๋ถ„์ ์œผ๋กœ๋Š” useMemo์— ๋Œ€ํ•ด ์ด์•ผ๊ธฐํ•ด๋ณด๋ ค ํ•œ๋‹ค.
ํŠนํžˆ ๋‚ด๊ฐ€ ๋ณด๊ธฐ์—” ์ •๋ง ๋ถˆํ•„์š”ํ•˜๊ฒŒ ์‚ฌ์šฉ๋˜๋Š” ๊ฒฝ์šฐ๋“ค์— ๋Œ€ํ•ด ๋ง์ด๋‹ค.

 

์™œ ๋ฉ”๋ชจ์ด์ œ์ด์…˜์„ ์‚ฌ์šฉํ• ๊นŒ?

๋ณดํ†ต useCallback์œผ๋กœ ํ•จ์ˆ˜๋ฅผ, useMemo๋กœ ๊ฐ’์„ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ํ•˜๋Š” ์ด์œ ๋Š” ๋‹จ ๋‘ ๊ฐ€์ง€๋ฟ์ด๋‹ค.

 

1. ์„ฑ๋Šฅ ์ตœ์ ํ™”(Performance optimization)

์–ด๋–ค ๋™์ž‘์ด ๋А๋ฆด ๋•Œ, ์šฐ๋ฆฌ๋Š” ๋ณดํ†ต ๊ทธ๊ฒƒ์„ “๋‚˜์˜๋‹ค”๊ณ  ๋А๋‚€๋‹ค. ์ด์ƒ์ ์œผ๋กœ๋Š” ๋” ๋น ๋ฅด๊ฒŒ ๋งŒ๋“œ๋Š” ๊ฒŒ ์ข‹์ง€๋งŒ, ํ•ญ์ƒ ๊ทธ๋ ‡๊ฒŒ ํ•  ์ˆ˜๋Š” ์—†๋‹ค. ๋Œ€์‹  ๊ทธ ๋А๋ฆฐ ๋™์ž‘์ด ์ผ์–ด๋‚˜๋Š” ํšŸ์ˆ˜๋ฅผ ์ค„์ด๋Š” ๋ฐฉ๋ฒ•์„ ์‹œ๋„ํ•  ์ˆ˜ ์žˆ๋‹ค.

React์—์„œ๋Š” ์ด๋Ÿฐ “๋А๋ฆฐ ๋™์ž‘”์ด ์ฃผ๋กœ ํ•˜์œ„ ํŠธ๋ฆฌ(sub-tree)์˜ ๋ฆฌ๋ Œ๋”๋ง์ด๋‹ค. ๋”ฐ๋ผ์„œ “๊ตณ์ด ํ•„์š”ํ•˜์ง€ ์•Š๋‹ค”๊ณ  ํŒ๋‹จ๋˜๋ฉด ์ด๋ฅผ ํ”ผํ•˜๊ณ  ์‹ถ์–ด์ง„๋‹ค.

 

์ด๋Ÿฐ ์ด์œ ๋กœ ๊ฐ€๋” React.memo๋กœ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ฐ์‹ธ๊ธฐ๋„ ํ•œ๋‹ค. ๋ฌผ๋ก  ์ด๋Š” ๋Œ€๋ถ€๋ถ„ “๋…ธ๋ ฅ ๋Œ€๋น„ ํšจ๊ณผ๊ฐ€ ํฌ์ง€ ์•Š์€ ์‹ธ์›€”์ด์ง€๋งŒ, ๊ทธ๋ž˜๋„ ์กด์žฌํ•˜๋Š” ํ…Œํฌ๋‹‰์ด๋‹ค.

๊ทธ๋Ÿฐ๋ฐ ๋ฉ”๋ชจ์ด์ œ์ด์…˜๋œ ์ปดํฌ๋„ŒํŠธ์— ํ•จ์ˆ˜๋ฅผ ์ „๋‹ฌํ•˜๊ฑฐ๋‚˜, ์›์‹œ ํƒ€์ž…์ด ์•„๋‹Œ ๊ฐ’์„ props๋กœ ๋„˜๊ธธ ๊ฒฝ์šฐ์—” ์ฐธ์กฐ(reference)๊ฐ€ ์•ˆ์ •์ ์ด์–ด์•ผ ํ•œ๋‹ค.
๊ทธ ์ด์œ ๋Š” React๊ฐ€ Object.is๋ฅผ ์‚ฌ์šฉํ•ด props๋ฅผ ๋น„๊ตํ•˜๊ณ , ๋™์ผํ•˜๋‹ค๊ณ  ํŒ๋‹จ๋˜๋ฉด ํ•ด๋‹น ํ•˜์œ„ ํŠธ๋ฆฌ์˜ ๋ฆฌ๋ Œ๋”๋ง์„ ๊ฑด๋„ˆ๋›ฐ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

๋งŒ์•ฝ ์ด ์ฐธ์กฐ๊ฐ€ ์•ˆ์ •์ ์ด์ง€ ์•Š๋‹ค๋ฉด — ์˜ˆ๋ฅผ ๋“ค์–ด, ๋งค ๋ Œ๋”๋ง๋งˆ๋‹ค ์ƒˆ๋กญ๊ฒŒ ์ƒ์„ฑ๋˜๋Š” ๊ฐ’์ด๋ผ๋ฉด — ์šฐ๋ฆฌ์˜ ๋ฉ”๋ชจ์ด์ œ์ด์…˜์€ ๊นจ์ง€๊ฒŒ ๋œ๋‹ค.

 

function Meh() {
  return (
    <MemoizedComponent
      value={{ hello: 'world' }}
      onChange={(result) => console.log('result')}
    />
  )
}

function Okay() {
  const value = useMemo(() => ({ hello: 'world' }), [])
  const onChange = useCallback((result) => console.log(result), [])

  return <MemoizedComponent value={value} onChange={onChange} />
}

 

๋ฌผ๋ก  ๋•Œ๋กœ๋Š” useMemo ๋‚ด๋ถ€์˜ ๊ณ„์‚ฐ ์ž์ฒด๊ฐ€ ๋А๋ฆด ๋•Œ๊ฐ€ ์žˆ๋‹ค.
์ด๋Ÿด ๋• ๋ถˆํ•„์š”ํ•œ ์žฌ๊ณ„์‚ฐ์„ ํ”ผํ•˜๊ธฐ ์œ„ํ•ด ๋ฉ”๋ชจ์ด์ œ์ด์…˜์„ ์‚ฌ์šฉํ•˜๋Š”๋ฐ, ์ด๋Ÿฐ ๊ฒฝ์šฐ์˜ useMemo ์‚ฌ์šฉ์€ ์ „ํ˜€ ๋ฌธ์ œ ์—†๋‹ค.
ํ•˜์ง€๋งŒ ์†”์งํžˆ ๋งํ•˜์ž๋ฉด, ์ด๋Ÿฐ ๊ฒฝ์šฐ๊ฐ€ ๋Œ€๋ถ€๋ถ„์€ ์•„๋‹ˆ๋‹ค.

 

 

2. Effect๊ฐ€ ๋ถˆํ•„์š”ํ•˜๊ฒŒ ์ž์ฃผ ์‹คํ–‰๋˜๋Š” ๊ฒƒ์„ ๋ง‰๊ธฐ ์œ„ํ•ด

๋ฉ”๋ชจ์ด์ œ์ด์…˜๋œ ๊ฐ’์„ ๋ฉ”๋ชจ๋œ ์ปดํฌ๋„ŒํŠธ์˜ props๋กœ ๋„˜๊ธฐ์ง€ ์•Š๋”๋ผ๋„,
๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ ๊ทธ ๊ฐ’์€ ๊ฒฐ๊ตญ ์–ด๋–ค Effect์˜ ์˜์กด์„ฑ(dependency) ์œผ๋กœ ์ „๋‹ฌ๋œ๋‹ค.
(๋•Œ๋กœ๋Š” ์—ฌ๋Ÿฌ ๋‹จ๊ณ„์˜ ์ปค์Šคํ…€ ํ›…์„ ๊ฑฐ์ณ์„œ ์ „๋‹ฌ๋˜๊ธฐ๋„ ํ•œ๋‹ค.)

Effect์˜ ์˜์กด์„ฑ ๋น„๊ต ๋ฐฉ์‹์€ React.memo์™€ ๋™์ผํ•˜๋‹ค.
React๋Š” ๊ฐ ์˜์กด์„ฑ์„ Object.is๋กœ ํ•˜๋‚˜์”ฉ ๋น„๊ตํ•ด, ๊ฐ’์ด ๋ฐ”๋€Œ์—ˆ๋‹ค๋ฉด Effect๋ฅผ ๋‹ค์‹œ ์‹คํ–‰ํ•œ๋‹ค.

๋”ฐ๋ผ์„œ Effect์˜ ์˜์กด์„ฑ ๊ฐ’์„ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ํ•˜์ง€ ์•Š์œผ๋ฉด,
๋ Œ๋”๋ง์ด ์ผ์–ด๋‚  ๋•Œ๋งˆ๋‹ค Effect๊ฐ€ ๋งค๋ฒˆ ์žฌ์‹คํ–‰๋˜๋Š” ์ƒํ™ฉ์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค.

 

๊ฐ€๋งŒํžˆ ์ƒ๊ฐํ•ด๋ณด๋ฉด, ์ด ๋‘ ๊ฐ€์ง€ ์ƒํ™ฉ์€ ์‚ฌ์‹ค ์™„์ „ํžˆ ๊ฐ™์€ ๋งฅ๋ฝ์ด๋ผ๋Š” ๊ฑธ ์•Œ ์ˆ˜ ์žˆ๋‹ค.
๋‘˜ ๋‹ค “๊ฐ™์€ ์ฐธ์กฐ(reference)๋ฅผ ์œ ์ง€ํ•ด์„œ ์–ด๋–ค ์ผ์ด ์ผ์–ด๋‚˜์ง€ ์•Š๋„๋ก ๋ง‰๋Š” ๊ฒƒ”์ด ๋ชฉ์ ์ด๋‹ค.

์ฆ‰, useCallback์ด๋‚˜ useMemo๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฐ€์žฅ ๊ณตํ†ต๋œ ์ด์œ ๋Š” ๊ฒฐ๊ตญ ์ด๊ฑฐ๋‹ค:

 

 

์ฐธ์กฐ์˜ ์•ˆ์ •์„ฑ์ด ํ•„์š”ํ•˜๋‹ค.
์ฆ‰, ๋งค ๋ Œ๋”๋ง๋งˆ๋‹ค ๊ฐ’์ด๋‚˜ ํ•จ์ˆ˜๊ฐ€ ์ƒˆ๋กœ ์ƒ์„ฑ๋˜์ง€ ์•Š๊ณ  
๊ฐ™์€ ์ฐธ์กฐ๋ฅผ ์œ ์ง€ํ•˜๋„๋ก ํ•˜๋Š” ๊ฒƒ,
๊ทธ๊ฒŒ ๋ฐ”๋กœ useCallback๊ณผ useMemo๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ํ•ต์‹ฌ ์ด์œ ๋‹ค.

 

์šฐ๋ฆฌ ๋ชจ๋‘ ์ธ์ƒ์—์„œ ์–ด๋А ์ •๋„์˜ ์•ˆ์ •์„ฑ(stability)์€ ํ•„์š”ํ•˜๋‹ค๊ณ  ์ƒ๊ฐํ•œ๋‹ค.
ํ•˜์ง€๋งŒ ์ฒ˜์Œ์— ๋งํ–ˆ๋“ฏ์ด, ๊ทธ ์•ˆ์ •์„ฑ์„ ๊ตณ์ด ์ถ”๊ตฌํ•  ํ•„์š”๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ, ์ฆ‰ ‘์“ธ๋ฐ์—†๋Š” ์•ˆ์ •์„ฑ ์ถ”๊ตฌ’๋Š” ์–ธ์ œ์ผ๊นŒ?

 


 

1.  ๋ฉ”๋ชจ์ด์ œ์ด์…˜์ด ์—†๋‹ค๊ณ  ํ•ด์„œ ์„ฑ๋Šฅ ํ–ฅ์ƒ์ด ์‚ฌ๋ผ์ง€๋Š” ๊ฑด ์•„๋‹ˆ๋‹ค

์•ž์„œ ์‚ดํŽด๋ณธ ์˜ˆ์‹œ์—์„œ, ์•„์ฃผ ์‚ฌ์†Œํ•œ ๋ถ€๋ถ„๋งŒ ํ•œ๋ฒˆ ๋ฐ”๊ฟ”๋ณด์ž.

 

no-memo

function Okay() {
  const value = useMemo(() => ({ hello: 'world' }), [])
  const onChange = useCallback((result) => console.log(result), [])

  return <Component value={value} onChange={onChange} />
}

 

์ฐจ์ด์ ์„ ์ฐพ์„ ์ˆ˜ ์žˆ๋Š”๊ฐ€? ๋งž๋‹ค — ์ด์ œ value์™€ onChange๋ฅผ ๋ฉ”๋ชจ์ด์ œ์ด์…˜๋œ ์ปดํฌ๋„ŒํŠธ์— ์ „๋‹ฌํ•˜์ง€ ์•Š๋Š”๋‹ค.
๊ทธ๋ƒฅ ์ผ๋ฐ˜์ ์ธ React ํ•จ์ˆ˜ํ˜• ์ปดํฌ๋„ŒํŠธ์ผ ๋ฟ์ด๋‹ค.

์ด๋Ÿฐ ์ƒํ™ฉ์€ ํŠนํžˆ ๊ฒฐ๊ตญ์—” ๊ฐ’์ด React์˜ ๊ธฐ๋ณธ(built-in) ์ปดํฌ๋„ŒํŠธ๋กœ ์ „๋‹ฌ๋  ๋•Œ ์ž์ฃผ ๋ฐœ์ƒํ•œ๋‹ค:

 

react-built-ins

function MyButton() {
  const onClick = useCallback(
    (event) => console.log(event.currentTarget.value),
    []
  )

  return <button onClick={onClick} />
}

 

 

์—ฌ๊ธฐ์„œ onClick์„ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ํ•ด๋„ ์•„๋ฌด๋Ÿฐ ํšจ๊ณผ๊ฐ€ ์—†๋‹ค.
์™œ๋ƒํ•˜๋ฉด ๋ฒ„ํŠผ ์ปดํฌ๋„ŒํŠธ๋Š” onClick์ด ์ฐธ์กฐ๊ฐ€ ์•ˆ์ •์ ์ธ์ง€ ์—ฌ๋ถ€๋ฅผ ์‹ ๊ฒฝ ์“ฐ์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

 

“์•„๋ฌด ํšจ๊ณผ๊ฐ€ ์—†๋‹ค(Achieves nothing)”๋ผ๊ณ  ํ‘œํ˜„ํ–ˆ์ง€๋งŒ, ์—„๋ฐ€ํžˆ ๋งํ•˜๋ฉด ์•ฝ๊ฐ„ ์ž˜๋ชป๋œ ํ‘œํ˜„์ด๋‹ค. ์‚ฌ์‹ค ๋‚ด๋ถ€์ ์œผ๋กœ๋Š” ๋ถ„๋ช… ๋ฌด์–ธ๊ฐ€ ์ผ์ด ์ผ์–ด๋‚˜๊ณ  ์žˆ๋‹ค.

React๋Š” onClick ํ•จ์ˆ˜๋ฅผ ์œ ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ์บ์‹œ๋ฅผ ๋งŒ๋“ค์–ด์•ผ ํ•˜๊ณ , ์˜์กด์„ฑ์„ ์ถ”์ ํ•˜๋ฉฐ ๋งค ๋ Œ๋”๋ง๋งˆ๋‹ค ๋น„๊ต๋ฅผ ์ˆ˜ํ–‰ํ•œ๋‹ค. useCallback์— ์ „๋‹ฌ๋œ ์ธ๋ผ์ธ ํ•จ์ˆ˜๋„ ๋งค ๋ Œ๋”๋งˆ๋‹ค ์ƒˆ๋กœ ์ƒ์„ฑ๋˜์ง€๋งŒ, ์บ์‹œ๋œ ๋ฒ„์ „์ด ๋ฐ˜ํ™˜๋˜๋ฉด ์ฆ‰์‹œ ๋ฒ„๋ ค์ง„๋‹ค.

๊ฒฐ๊ณผ์ ์œผ๋กœ, ๊ธฐ์ˆ ์ ์œผ๋กœ๋Š” ๋‚ด๋ถ€์— ์•ฝ๊ฐ„์˜ ์˜ค๋ฒ„ํ—ค๋“œ๊ฐ€ ์ƒ๊ธด๋‹ค. ํ•˜์ง€๋งŒ ๋‚˜๋Š” ์ด ์˜ค๋ฒ„ํ—ค๋“œ์— ๋„ˆ๋ฌด ์ง‘์ค‘ํ•˜๊ณ  ์‹ถ์ง„ ์•Š๋‹ค. ์™œ๋ƒํ•˜๋ฉด ์ง„์งœ ๋ฌธ์ œ๋Š” ๊ทธ๊ฒŒ ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

 

 

์ฆ‰, ์‚ฌ์šฉ์ž ์ •์˜ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ฉ”๋ชจ์ด์ œ์ด์…˜๋˜์–ด ์žˆ์ง€ ์•Š๋‹ค๋ฉด, ์ฐธ์กฐ ์•ˆ์ •์„ฑ(referential stability)์€ ์‚ฌ์‹ค ์‹ ๊ฒฝ ์“ธ ํ•„์š”๊ฐ€ ์—†๋‹ค๋Š” ๋œป์ด๋‹ค.

 

ํ•˜์ง€๋งŒ ์ž ๊น — ๋งŒ์•ฝ ๊ทธ ์ปดํฌ๋„ŒํŠธ๊ฐ€ props๋ฅผ ๋‚ด๋ถ€์—์„œ useEffect์˜ ์˜์กด์„ฑ์œผ๋กœ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜, ๋‹ค๋ฅธ ๋ฉ”๋ชจ์ด์ œ์ด์…˜๋œ ๊ฐ’์„ ๋งŒ๋“ค์–ด ๊ทธ ๊ฐ’์„ ์ž์‹ ์ปดํฌ๋„ŒํŠธ์— ์ „๋‹ฌํ•œ๋‹ค๋ฉด ์–ด๋–ป๊ฒŒ ๋ ๊นŒ?


์ด ๊ฒฝ์šฐ, ์ง€๊ธˆ useMemo๋‚˜ useCallback์„ ์ œ๊ฑฐํ•˜๋ฉด , ๊ธฐ์กด ๋ฉ”๋ชจ์ด์ œ์ด์…˜์„ ์ œ๊ฑฐํ•˜๋ฉด ๋ฌด์–ธ๊ฐ€๊ฐ€ ๊นจ์งˆ ์ˆ˜๋„ ์žˆ๋‹ค.

๋ฐ”๋กœ ์ด ์ ์ด ๋‘ ๋ฒˆ์งธ ํฌ์ธํŠธ๋กœ ์ด์–ด์ง„๋‹ค.

 

2. props๋ฅผ ์˜์กด์„ฑ์œผ๋กœ ์‚ฌ์šฉํ•  ๋•Œ

์ปดํฌ๋„ŒํŠธ์— ์ „๋‹ฌ๋ฐ›์€ ์›์‹œ ํƒ€์ž…์ด ์•„๋‹Œ(non-primitive) ๋น„์›์‹œ๊ฐ’ props๋ฅผ ๋‚ด๋ถ€ useEffect๋‚˜ useMemo ๋“ฑ์˜ ์˜์กด์„ฑ ๋ฐฐ์—ด์— ๊ทธ๋Œ€๋กœ ๋„ฃ๋Š” ๊ฑด์€ ๊ฑฐ์˜ ๋Œ€๋ถ€๋ถ„ ์ ์ ˆํ•˜์ง€ ์•Š๋‹ค.


์™œ๋ƒํ•˜๋ฉด ์ด ์ปดํฌ๋„ŒํŠธ๋Š” ๊ทธ props์˜ ์ฐธ์กฐ ์•ˆ์ •์„ฑ์— ๋Œ€ํ•ด ์–ด๋–ค ํ†ต์ œ๊ถŒ๋„ ์—†๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

ํ”ํ•œ ์˜ˆ์‹œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

 

props-as-dependencies

function OhNo({ onChange }) {
  const handleChange = useCallback((e: React.ChangeEvent) => {
    trackAnalytics('changeEvent', e)
    onChange?.(e)
  }, [onChange])

  return <SomeMemoizedComponent onChange={handleChange} />
}

 

 

์ด useCallback์€ ์•„๋งˆ ์“ธ๋ชจ๊ฐ€ ์—†๊ฑฐ๋‚˜,
๊ธฐ๊ปํ•ด์•ผ ์ด ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ชฝ์ด ์–ด๋–ป๊ฒŒ ์‚ฌ์šฉํ•˜๋А๋ƒ์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์งˆ ๋ฟ์ด๋‹ค.

๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ, ํ˜ธ์ถœํ•˜๋Š” ์ชฝ์—์„œ ๊ทธ๋ƒฅ ์ธ๋ผ์ธ ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•  ๊ฐ€๋Šฅ์„ฑ์ด ๋†’๋‹ค.

 

<OhNo onChange={() => props.doSomething()} />โ€‹

 

 

์ด๊ฑด ๋ฌดํ•ดํ•œ ์‚ฌ์šฉ ์‚ฌ๋ก€๋‹ค. ์ „ํ˜€ ๋ฌธ์ œ๋  ๊ฒŒ ์—†๋‹ค.
์‚ฌ์‹ค ์˜คํžˆ๋ ค ์ข‹์€ ํŒจํ„ด์ด๋ผ๊ณ  ๋ณผ ์ˆ˜ ์žˆ๋‹ค.
์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ์™€ ๊ด€๋ จ๋œ ๋™์ž‘์„ ํ•œ๊ณณ์— ๋ชจ์•„๋‘์–ด, ํŒŒ์ผ ์ƒ๋‹จ์œผ๋กœ ๋Œ์–ด์˜ฌ๋ ค์„œ ๊ธธ๊ฒŒ ์ด๋ฆ„ ๋ถ™์ธ handleChange ๊ฐ™์€ ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“œ๋Š” ์ผ์„ ํ”ผํ•  ์ˆ˜ ์žˆ๋‹ค.

 

์ด ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•œ ๊ฐœ๋ฐœ์ž๊ฐ€ ๋ฉ”๋ชจ์ด์ œ์ด์…˜์ด ๊นจ์ง„๋‹ค๋Š” ์‚ฌ์‹ค์„ ์•Œ ์ˆ˜ ์žˆ๋Š” ์œ ์ผํ•œ ๋ฐฉ๋ฒ•์€, ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€๋ฅผ ๊นŠ์ด ํŒŒ๊ณ  ๋“ค์–ด๊ฐ€ props๊ฐ€ ์–ด๋–ป๊ฒŒ ์‚ฌ์šฉ๋˜๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๊ฒƒ๋ฟ์ด๋‹ค.
์†”์งํžˆ ๋งํ•ด, ๊ทธ๊ฑด ๋”์ฐํ•˜๋‹ค.

 

๋‹ค๋ฅธ ํ•ด๊ฒฐ์ฑ…์œผ๋กœ๋Š” “๋ชจ๋“  ๊ฑธ ํ•ญ์ƒ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ํ•œ๋‹ค”๋ผ๋Š” ์ •์ฑ…์„ ์„ธ์šฐ๊ฑฐ๋‚˜,
์ฐธ์กฐ ์•ˆ์ •์„ฑ์ด ํ•„์š”ํ•œ props์—๋Š” mustBeMemoized ๊ฐ™์€ ์—„๊ฒฉํ•œ ๋„ค์ด๋ฐ ๊ทœ์น™์„ ์ ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ์žˆ๋‹ค.
ํ•˜์ง€๋งŒ ์ด ๋‘˜ ๋‹ค ์ข‹์€ ๋ฐฉ๋ฒ•์€ ์•„๋‹ˆ๋‹ค.

 

์‹ค์ œ ์‚ฌ๋ก€(Real Life Example)

์ง€๊ธˆ ๋‚˜๋Š” Sentry ์ฝ”๋“œ๋ฒ ์ด์Šค์—์„œ ์ž‘์—…ํ•˜๊ณ  ์žˆ๋Š”๋ฐ, ์ด๊ฑด ์˜คํ”ˆ์†Œ์Šค๋ผ์„œ ๐ŸŽ‰ ์‹ค์ œ ์‚ฌ์šฉ ์‚ฌ๋ก€๋ฅผ ์‰ฝ๊ฒŒ ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ๋‹ค.

๋‚ด๊ฐ€ ๋ฐœ๊ฒฌํ•œ ํ•œ ๊ฐ€์ง€ ์ƒํ™ฉ์€ useHotkeys ์ปค์Šคํ…€ ํ›…์ด๋‹ค.
ํ•ต์‹ฌ ๋ถ€๋ถ„์€ ๋Œ€๋žต ์ด๋ ‡๊ฒŒ ์ƒ๊ฒผ๋‹ค:

 

useHotkeys

export function useHotkeys(hotkeys: Hotkey[]): {
  const onKeyDown = useCallback(() => ..., [hotkeys])

  useEffect(() => {
    document.addEventListener('keydown', onKeyDown)

    return () => {
      document.removeEventListener('keydown', onKeyDown)
    }
  }, [onKeyDown])
}

 

 

์ด ์ปค์Šคํ…€ ํ›…์€ hotKeys ๋ฐฐ์—ด์„ ์ธ์ž๋กœ ๋ฐ›๊ณ ,
๊ทธ๊ฑธ ๊ธฐ๋ฐ˜์œผ๋กœ onKeyDown ํ•จ์ˆ˜๋ฅผ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ํ•œ ๋’ค, ํ•ด๋‹น ํ•จ์ˆ˜๋ฅผ effect์— ๋„˜๊ธด๋‹ค.

 

ํ•จ์ˆ˜๋ฅผ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ํ•˜๋Š” ์ด์œ ๋Š” ๋ช…๋ฐฑํ•˜๋‹ค: Effect๊ฐ€ ๋„ˆ๋ฌด ์ž์ฃผ ์‹คํ–‰๋˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋‹ค.


ํ•˜์ง€๋งŒ ์ž…๋ ฅ๊ฐ’์ธ hotkeys๊ฐ€ ๋ฐฐ์—ด์ด๊ธฐ ๋•Œ๋ฌธ์—, ์‚ฌ์šฉํ•˜๋Š” ์ชฝ์—์„œ ์ง์ ‘ hotkeys๋ฅผ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ํ•ด์ฃผ์–ด์•ผ ํ•œ๋‹ค.

 

๊ทธ๋ž˜์„œ ๋‚˜๋Š” useHotkeys๊ฐ€ ์‹ค์ œ ์ฝ”๋“œ์—์„œ ์–ด๋–ป๊ฒŒ ์“ฐ์ด๊ณ  ์žˆ๋Š”์ง€ ์ „์ˆ˜ ์กฐ์‚ฌ๋ฅผ ํ•ด๋ดค๋‹ค.
๋†€๋ž๊ฒŒ๋„ ๊ฑฐ์˜ ๋Œ€๋ถ€๋ถ„(ํ•œ ๊ณณ์„ ์ œ์™ธํ•˜๊ณ )์€ ์ž…๋ ฅ๊ฐ’์„ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ํ•˜๊ณ  ์žˆ์—ˆ๋‹ค.

 

ํ•˜์ง€๋งŒ ์ด์•ผ๊ธฐ๋Š” ์—ฌ๊ธฐ์„œ ๋๋‚˜์ง€ ์•Š๋Š”๋‹ค.
๋” ๊นŠ์ด ๋“ค์–ด๊ฐ€ ๋ณด๋ฉด, ๊ฒฐ๊ตญ์—๋Š” ์—ฌ์ „ํžˆ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธฐ๊ธฐ ์‰ฌ์šด ๊ตฌ์กฐ๋ผ๋Š” ๊ฑธ ์•Œ ์ˆ˜ ์žˆ๋‹ค.
์˜ˆ๋ฅผ ๋“ค์–ด, ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์‚ฌ์šฉ ์‚ฌ๋ก€๋ฅผ ๋ณด์ž.

 

 

paginateHotKeys

const paginateHotkeys = useMemo(() => {
  return [
    { match: 'right', callback: () => paginateItems(1) },
    { match: 'left', callback: () => paginateItems(-1) },
  ]
}, [paginateItems])

useHotkeys(paginateHotkeys)

 

 

useHotKeys๋Š” ๋ฉ”๋ชจ์ด์ œ์ด์…˜๋œ paginateHotkeys๋ฅผ ์ „๋‹ฌํ•œ๋‹ค.
๊ทธ๋Ÿฐ๋ฐ ์ด paginateHotkeys๋Š” paginateItems์— ์˜์กดํ•œ๋‹ค.

๊ทธ๋Ÿผ paginateItems๋Š” ์–ด๋””์„œ ์˜ฌ๊นŒ?


๋ฐ”๋กœ ๋˜ ๋‹ค๋ฅธ useCallback์ด๊ณ , ์ด ์ฝœ๋ฐฑ์€ screenshots์™€ currentAttachmentIndex์— ์˜์กดํ•œ๋‹ค.

๊ทธ๋ ‡๋‹ค๋ฉด screenshots๋Š” ์–ด๋””์„œ ์˜ค๋Š” ๊ฑธ๊นŒ?

 

breaking-memoization

const screenshots = attachments.filter(({ name }) =>
  name.includes('screenshot')
)

 

screenshots๋Š” ๋ฉ”๋ชจ์ด์ œ์ด์…˜๋˜์ง€ ์•Š์€ attachments.filter ํ•จ์ˆ˜์—์„œ ์ƒ์„ฑ๋œ๋‹ค.
์ด ํ•จ์ˆ˜๋Š” ๋งค๋ฒˆ ์ƒˆ๋กœ์šด ๋ฐฐ์—ด(Array)์„ ์ƒ์„ฑํ•˜๋ฏ€๋กœ, downstream์— ์žˆ๋Š” ๋ชจ๋“  ๋ฉ”๋ชจ์ด์ œ์ด์…˜์ด ๊นจ์ง„๋‹ค.

๊ฒฐ๊ณผ์ ์œผ๋กœ ๋ชจ๋“  ๋ฉ”๋ชจ์ด์ œ์ด์…˜์ด ๋ฌด์šฉ์ง€๋ฌผ์ด ๋œ๋‹ค.


์ฆ‰, paginateItems, paginateHotkeys, onKeyDown
์ด ์„ธ ๊ฐ€์ง€ ๋ฉ”๋ชจ์ด์ œ์ด์…˜์ด ๋งค ๋ Œ๋”๋ง๋งˆ๋‹ค ๋‹ค์‹œ ์‹คํ–‰๋˜๋ฉฐ,
๋งˆ์น˜ ์•„์˜ˆ ์ž‘์„ฑํ•˜์ง€ ์•Š์€ ๊ฒƒ๊ณผ ๋™์ผํ•œ ์ƒํ™ฉ์ด ๋œ๋‹ค! (์ด๋ ‡๊ฒŒ ๋˜๋ฉด ์šฐ๋ฆฌ๊ฐ€ ํ–ˆ๋˜ ๋ฉ”๋ชจ์ด์ œ์ด์…˜์€ ์ „๋ถ€ ๋ฌด์šฉ์ง€๋ฌผ์ด ๋œ๋‹ค)

 

 

์ด ์˜ˆ์‹œ๊ฐ€ ๋‚ด๊ฐ€ ์™œ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ ์ ์šฉ์— ๋ฐ˜๋Œ€ํ•˜๋Š” ์ž…์žฅ์ธ์ง€ ์ž˜ ๋ณด์—ฌ์คฌ์œผ๋ฉด ํ•œ๋‹ค.
๋‚ด ๊ฒฝํ—˜์ƒ, ๋ฉ”๋ชจ์ด์ œ์ด์…˜์€ ๋„ˆ๋ฌด ์ž์ฃผ ๊นจ์ง€๊ณ , ๊ทธ๋Ÿด ๊ฐ€์น˜๊ฐ€ ์—†๋‹ค.


๊ฒŒ๋‹ค๊ฐ€ ์ฝ”๋“œ ์ „์ฒด์— ์—„์ฒญ๋‚œ ์˜ค๋ฒ„ํ—ค๋“œ์™€ ๋ณต์žก์„ฑ์„ ์ถ”๊ฐ€ํ•œ๋‹ค.

์—ฌ๊ธฐ์„œ ํ•ด๊ฒฐ์ฑ…์ด screenshots๊นŒ์ง€ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ํ•˜๋Š” ๊ฒƒ์€ ์•„๋‹ˆ๋‹ค.


๊ทธ๋ ‡๊ฒŒ ํ•˜๋ฉด ์ฑ…์ž„์ด ๊ฒฐ๊ตญ ์ปดํฌ๋„ŒํŠธ์˜ prop์ธ attachments๋กœ ๋„˜์–ด๊ฐ€๊ฒŒ ๋œ๋‹ค.
์„ธ ๊ฐ€์ง€ ํ˜ธ์ถœ ์ง€์ (call-side) ๋ชจ๋‘์—์„œ ์‹ค์ œ๋กœ ๋ฉ”๋ชจ์ด์ œ์ด์…˜์ด ํ•„์š”ํ•œ useHotkeys์™€๋Š” ์ตœ์†Œ ๋‘ ๋‹จ๊ณ„ ์ด์ƒ ๋–จ์–ด์ ธ ์žˆ๊ฒŒ ๋œ๋‹ค.


์ด๊ฑด ์ฝ”๋“œ๋ฅผ ํƒ์ƒ‰ํ•˜๊ธฐ ์•…๋ชฝ ๊ฐ™์€ ์ƒํ™ฉ์„ ๋งŒ๋“ค๋ฉฐ, ๊ฒฐ๊ตญ ์•„๋ฌด๋„ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ ํ•˜๋‚˜๋ฅผ ์ œ๊ฑฐํ•˜์ง€ ๋ชปํ•˜๊ฒŒ ๋œ๋‹ค.
์™œ๋ƒํ•˜๋ฉด ๊ทธ ๋ฉ”๋ชจ์ด์ œ์ด์…˜์ด ์‹ค์ œ๋กœ ๋ฌด์—‡์„ ํ•˜๊ณ  ์žˆ๋Š”์ง€ ์•Œ ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

์‹ค์ œ๋กœ๋Š”, ์ด๋Ÿฐ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋ ค๋ฉด ์ปดํŒŒ์ผ๋Ÿฌ์—๊ฒŒ ๋ชจ๋“  ๊ฑธ ๋งก๊ธฐ๋Š” ๊ฒƒ์ด ๊ฐ€์žฅ ์ข‹๋‹ค.


๋ชจ๋“  ํ™˜๊ฒฝ์—์„œ ์ž˜ ์ž‘๋™ํ•œ๋‹ค๋ฉด ์ •๋ง ํ›Œ๋ฅญํ•˜๋‹ค.
ํ•˜์ง€๋งŒ ๊ทธ ์ „๊นŒ์ง€๋Š”, ์ฐธ์กฐ ์•ˆ์ •์„ฑ(referential stability) ํ•„์š”์„ฑ์˜ ํ•œ๊ณ„๋ฅผ ์šฐํšŒํ•  ํŒจํ„ด์„ ์ฐพ์•„์•ผ ํ•œ๋‹ค.

 


 

 

์ตœ์‹  Ref ํŒจํ„ด(The Latest Ref Pattern)

์ด ํŒจํ„ด์— ๋Œ€ํ•ด์„œ๋Š” ์ด์ „์— ์–ธ๊ธ‰ํ•œ ์ ์ด ์žˆ๋‹ค.
ํ•ต์‹ฌ ์•„์ด๋””์–ด๋Š”, Effect ์•ˆ์—์„œ ์ฆ‰์‹œ ์ ‘๊ทผํ•˜๊ณ  ์‹ถ์€ ๊ฐ’์„ ref ์•ˆ์— ์ €์žฅํ•ด๋‘๊ณ ,
๋˜ ๋‹ค๋ฅธ Effect๋ฅผ ํ†ตํ•ด ์˜๋„์ ์œผ๋กœ ๋งค ๋ Œ๋”๋ง๋งˆ๋‹ค ๊ฐ’์„ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

 

the-latest-ref

export function useHotkeys(hotkeys: Hotkey[]): {
  const hotkeysRef = useRef(hotkeys)

  useEffect(() => {
    hotkeysRef.current = hotkeys
  })

  const onKeyDown = useCallback(() => ..., [])

  useEffect(() => {
    document.addEventListener('keydown', onKeyDown)

    return () => {
      document.removeEventListener('keydown', onKeyDown)
    }
  }, [])
}

 

 

๊ทธ๋ ‡๊ฒŒ ํ•˜๋ฉด hotkeysRef๋ฅผ Effect ์•ˆ์—์„œ ๋ฐ”๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
์˜์กด์„ฑ ๋ฐฐ์—ด์— ์ถ”๊ฐ€ํ•  ํ•„์š”๋„ ์—†๊ณ , ๋‹จ์ˆœํžˆ ๋ฆฐํ„ฐ๋ฅผ ๋ฌด์‹œํ•˜๋‹ค๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” stale closure ๋ฌธ์ œ๋„ ๊ฑฑ์ •ํ•˜์ง€ ์•Š์•„๋„ ๋œ๋‹ค.

์‹ค์ œ๋กœ React Query๋„ ์ด ํŒจํ„ด์„ ์‚ฌ์šฉํ•œ๋‹ค.


์˜ˆ๋ฅผ ๋“ค์–ด PersistQueryClientProvider๋‚˜ useMutationState์—์„œ ์ตœ์‹  ์˜ต์…˜(latest options)์„ ์ถ”์ ํ•  ๋•Œ ๊ทธ๋ ‡๋‹ค.
์ฆ‰, ๊ฒ€์ฆ๋œ(tried-and-true) ํŒจํ„ด์ด๋ผ๊ณ  ๋ณผ ์ˆ˜ ์žˆ๋‹ค.
๋งŒ์•ฝ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์‚ฌ์šฉ์ž๊ฐ€ ์˜ต์…˜์„ ์ง์ ‘ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ํ•˜๋„๋ก ๊ฐ•์ œํ•œ๋‹ค๋ฉด ์–ผ๋งˆ๋‚˜ ๊ท€์ฐฎ์„์ง€ ์ƒ์ƒํ•ด๋ณด๋ผ.

 

 

UseEffectEvent

์ข‹์€ ์†Œ์‹์ด ํ•˜๋‚˜ ๋” ์žˆ๋‹ค.
React ํŒ€์€ ๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ, ๋ฆฌ์•กํ‹ฐ๋ธŒ Effect ์•ˆ์—์„œ ์ตœ์‹  ๊ฐ’์„ ๋ช…๋ นํ˜•(imperative)์œผ๋กœ ์ ‘๊ทผํ•  ํ•„์š”๊ฐ€ ์žˆ์ง€๋งŒ, Effect๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ๋‹ค์‹œ ์‹คํ–‰ํ•˜๊ณ  ์‹ถ์ง€ ์•Š์€ ์ƒํ™ฉ์ด ์žˆ๋‹ค๋Š” ๊ฒƒ์„ ์ธ์‹ํ–ˆ๋‹ค.

๊ทธ๋ž˜์„œ ์ด ํŒจํ„ด์„ ์œ„ํ•œ 1๊ธ‰ ํ”„๋ฆฌ๋ฏธํ‹ฐ๋ธŒ(first-class primitive), useEffectEvent๋ฅผ ์ƒˆ๋กœ ์ถ”๊ฐ€ํ•  ์˜ˆ์ •์ด๋‹ค.

 

์ด ํ›…์ด ๋„์ž…๋˜๋ฉด, ๊ธฐ์กด ์ฝ”๋“œ๋ฅผ ๋ณด๋‹ค ๊น”๋”ํ•˜๊ณ  ์•ˆ์ „ํ•˜๊ฒŒ ๋ฆฌํŒฉํ„ฐ๋งํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค:

 

the-latest-ref

export function useHotkeys(hotkeys: Hotkey[]): {
  const onKeyDown = useEffectEvent(() => ...)

  useEffect(() => {
    document.addEventListener('keydown', onKeyDown)

    return () => {
      document.removeEventListener('keydown', onKeyDown)
    }
  }, [])
}

 

 

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด onKeyDown์€ ๋ฆฌ์•กํ‹ฐ๋ธŒํ•˜์ง€ ์•Š๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค. ํ•˜์ง€๋งŒ ์—ฌ์ „ํžˆ hotkeys์˜ ์ตœ์‹  ๊ฐ’์„ ํ•ญ์ƒ “๋ณผ ์ˆ˜ ์žˆ๊ฒŒ” ๋˜๋ฉฐ, ๋ Œ๋” ์‚ฌ์ด์—์„œ๋„ ์ฐธ์กฐ ์•ˆ์ •์„ฑ(referential stability) ์„ ์œ ์ง€ํ•œ๋‹ค.

์ฆ‰, useCallback์ด๋‚˜ useMemo๋ฅผ ๋‹จ ํ•˜๋‚˜๋„ ์“ฐ์ง€ ์•Š๊ณ ๋„, ๋ชจ๋“  ์žฅ์ ์„ ๋™์‹œ์— ์–ป์„ ์ˆ˜ ์žˆ๊ฒŒ ๋˜๋Š” ๊ฒƒ์ด๋‹ค.

 

 

์ •๋ฆฌ

๋‹จ์ˆœํžˆ ์„ฑ๋Šฅ ์ตœ์ ํ™”๋‚˜ ์•ˆ์ •์„ฑ์„ ์ด์œ ๋กœ ๋ฌด์ž‘์ • useMemo/useCallback์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์€ ๋Œ€๋ถ€๋ถ„ ๋ถˆํ•„์š”ํ•˜๊ฑฐ๋‚˜ ์˜คํžˆ๋ ค ๋ฌธ์ œ๋ฅผ ๋งŒ๋“ ๋‹ค.
๋Œ€์‹  Ref๋ฅผ ํ™œ์šฉํ•˜๊ฑฐ๋‚˜ ์ƒˆ๋กœ์šด React ํŒจํ„ด์„ ์‚ฌ์šฉํ•ด ์ตœ์‹  ๊ฐ’์„ ์•ˆ์ „ํ•˜๊ฒŒ ์ฐธ์กฐํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์“ฐ๋Š” ๊ฒŒ ์ข‹๋‹ค.

 

 

 

์›๋ฌธ: https://marmelab.com/blog/2024/01/23/react-19-new-hooks.html

 

 

์ผ๋ฐ˜์ ์ธ ์ƒ๊ฐ๊ณผ ๋‹ฌ๋ฆฌ React coreํŒ€์€ ๋ฆฌ์•กํŠธ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์™€ Next.js์—๋งŒ ์˜ค์ง ์ดˆ์ ์„ ๋‘์ง„ ์•Š๋Š”๋‹ค. ์ƒˆ๋กœ์šด client-side ํ›…๋“ค์ด ๋‹ค์Œ React ๋ฉ”์ธ ๋ฒ„์ „์ธ 19๋ฒ„์ „์— ๋„์ž…๋œ๋‹ค. ๊ทธ๋“ค์€ ๋ฆฌ์•กํŠธ์˜ ๋‘ ๊ฐ€์ง€ ์ฃผ์š” ๋ฌธ์ œ์ , ์ฆ‰ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ(data fetching)๊ณผ ํผ(form)์— ์ดˆ์ ์„ ๋งž์ถ”๊ณ  ์žˆ๋‹ค. ์ด ํ›…๋“ค์€ ์‹ฑ๊ธ€ ํŽ˜์ด์ง€ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ž‘์—…์„ ํ•˜๋Š” ๊ฐœ๋ฐœ์ž๋ฅผ ํฌํ•จํ•ด React ๊ฐœ๋ฐœ์ž๋“ค์˜ ์ƒ์‚ฐ์„ฑ์„ ํ–ฅ์ƒ์‹œ์ผœ์ค„ ๊ฒƒ์ด๋‹ค.

 

๋” ์ด์ƒ ๊ณ ๋ฏผํ•˜์ง€ ๋ง๊ณ , ์ƒˆ๋กœ์šด ํ›…๋“ค์„ ์‚ดํŽด๋ณด์ž!

 

 

์ฃผ๋ชฉ: ์ด ํ›…๋“ค์€ React์˜ Canary์™€ ์‹คํ—˜์  ์ฑ„๋„์—์„œ๋งŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด ํ›…๋“ค์€ ๊ณง ์ถœ์‹œ๋  ๋ฆฌ์•กํŠธ 19์— ํฌํ•จ๋˜์ง€๋งŒ, API๋Š” ์ตœ์ข… ์ถœ์‹œ ์ „์— ๋ณ€๊ฒฝ๋  ์ˆ˜ ์žˆ๋‹ค.

 

 

use(Promise)

 

์ด ์ƒˆ๋กœ์šด ํ›…์€ ํด๋ผ์ด์–ธํŠธ์˜ 'suspending'์„ ์œ„ํ•œ ๊ณต์‹ API์ด๋‹ค. ๋‹น์‹ ์€ ๊ทธ๊ฒƒ์„ promise๋กœ ๋„˜๊ธธ ์ˆ˜ ์žˆ๊ณ  ๋ฆฌ์•กํŠธ๋Š” ๊ทธ๊ฒƒ์ด ํ•ด๊ฒฐ๋  ๋•Œ๊นŒ์ง€ ๊ทธ๊ฒƒ์„ ์ค‘๋‹จ์‹œํ‚ฌ ๊ฒƒ์ด๋‹ค. React use ๋ฌธ์„œ์—์„œ ๊ฐ€์ ธ์˜จ ๊ธฐ๋ณธ ๊ตฌ๋ฌธ์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

 

import { use } from 'react';

function MessageComponent({ messagePromise }) {
    const message = use(messagePromise);
    // ...
}

 

์ข‹์€ ์†Œ์‹์€ ์ด ํ›…์ด ๋ฐ์ดํ„ฐ ํŒจ์นญ์— ์‚ฌ์šฉ๋  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค. ์—ฌ๊ธฐ ๋งˆ์šดํŠธ์‹œ์™€ ๋ฒ„ํŠผ ํด๋ฆญ์‹œ ๋ฐ์ดํ„ฐ ํŒจ์นญ์„ ํ•˜๋Š” ๊ตฌ์ฒด์ ์ธ ์˜ˆ์‹œ๊ฐ€ ์žˆ๋‹ค. ์ด ์ฝ”๋“œ๋Š” ๋‹จ์ผ useEffect๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค:

 

 

 

<Suspense> ๋ฌธ์„œ์˜ ์ด ๊ฒฝ๊ณ ๋ฅผ ๊ธฐ์–ตํ•˜๋Š”๊ฐ€?

 

ํ”„๋ ˆ์ž„์›Œํฌ์˜ ์˜๊ฒฌ ์—†๋Š” Suspense ์ง€์› ๋ฐ์ดํ„ฐ ํŒจ์นญ์€ ์•„์ง ์ง€์›๋˜์ง€ ์•Š๋Š”๋‹ค.

 

์ด๋Š” React19์—์„œ๋Š” ๋” ์ด์ƒ ์‚ฌ์‹ค์ด ์•„๋‹ˆ๋‹ค.

 

์ƒˆ๋กœ์šด use ํ›…์€ ์ˆจ๊ฒจ์ง„ ํž˜์ด ์žˆ๋‹ค: ๋‹ค๋ฅธ ๋ชจ๋“  React Hook๋“ค๊ณผ ๋‹ฌ๋ฆฌ, use๋Š” loop์™€ if์™€ ๊ฐ™์€ ์กฐ๊ฑด ์ ˆ์—์„œ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋‹ค.

 

์ด ๋ง์ด ์ฆ‰, ํด๋ผ์ด์–ธํŠธ ์ธก์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ธฐ ์œ„ํ•ด TanStack Query์™€ ๊ฐ™์€ ์„œ๋“œํŒŒํ‹ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๋” ์ด์ƒ ์‚ฌ์šฉํ•  ํ•„์š”๊ฐ€ ์—†์Œ์„ ์˜๋ฏธํ• ๊นŒ? TanStack Query๊ฐ€ ๋‹จ์ˆœํžˆ Promise๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ๊ฒƒ ์ด์ƒ์˜ ์—ญํ• ์„ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋” ๋‘๊ณ  ๋ด์•ผ ํ•œ๋‹ค.

 

ํ•˜์ง€๋งŒ ๊ทธ๊ฒƒ์€ ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉํ–ฅ์œผ๋กœ ๋‚˜์•„๊ฐ€๋Š” ํ›Œ๋ฅญํ•œ ๋‹จ๊ณ„์ด๋ฉฐ REST ๋˜๋Š” GraphQL API๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•œ ์‹ฑ๊ธ€ ํŽ˜์ด์ง€ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์‰ฝ๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ์„ ๊ฒƒ์ด๋‹ค. ๋‚˜๋Š” ์ด ์ƒˆ๋กœ์šด ํ›…์— ๋งค์šฐ ์—ด๊ด‘ํ•œ๋‹ค!

 

React ๋ฌธ์„œ์—์„œ use(Promise) ํ›…์— ๋Œ€ํ•œ ์ž์„ธํ•œ ๋‚ด์šฉ์„ ์ฝ์–ด๋ณด์•„๋ผ.

 

use(Context)

React Context๋ฅผ ์ฝ๋Š”๋ฐ ๊ฐ™์€ use ํ›…์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. ๋ฃจํ”„ ์•ˆ์—์„œ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ if์™€ ๊ฐ™์€ ์กฐ๊ฑด๋ฌธ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์ ์„ ์ œ์™ธํ•˜๋ฉด ์ด๋Š” ์ •ํ™•ํžˆ useContext์™€ ๊ฐ™๋‹ค.

 

 

import { use } from 'react';

function HorizontalRule({ show }) {
    if (show) {
        const theme = use(ThemeContext);
        return <hr className={theme} />;
    }
    return false;
}

 

๋ฃจํ”„ ํ˜น์€ ์กฐ๊ฑด์ ˆ์—์„œ ์ปจํ…์ŠคํŠธ๋ฅผ ์ฝ๋Š” ์œ ์ผํ•œ ๋ฐฉ๋ฒ•์ด ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋‘˜๋กœ ๋‚˜๋ˆ„๋Š” ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์— ์ด๊ฒƒ์€ ์ผ๋ถ€ ์‚ฌ์šฉ ์‚ฌ๋ก€์— ๋Œ€ํ•œ ์ปดํฌ๋„ŒํŠธ ๊ณ„์ธต ๊ตฌ์กฐ๋ฅผ ๋‹จ์ˆœํ™”ํ•  ๊ฒƒ์ด๋‹ค.

 

์ปจํ…์ŠคํŠธ๊ฐ€ ๋ฐ”๋€Œ์—ˆ๋”๋ผ๋„ ์ปดํฌ๋„ŒํŠธ์˜ ๋ฆฌ๋ Œ๋”๋ง์„ ์กฐ๊ฑด์ ์œผ๋กœ ๊ฑด๋„ˆ๋›ธ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์„ฑ๋Šฅ ์ธก๋ฉด์—์„œ๋„ ํฐ ํ˜๋ช…์ด ๋  ์ˆ˜ ์žˆ๋‹ค.

 

React ๋ฌธ์„œ์—์„œ use(Context) ํ›…์— ๋Œ€ํ•œ ์ž์„ธํ•œ ๋‚ด์šฉ์„ ์ฝ์–ด๋ณด์•„๋ผ.

 

Form Actions

 

์ด ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๋ฉด ํ•จ์ˆ˜๋ฅผ <form>์˜ ์•ก์…˜ ํ”„๋กญ์— ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ๋‹ค. ํผ์ด ์ œ์ถœ๋˜๋ฉด React๋Š” ์ด ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค:

 

 

 

 

๋งŒ์•ฝ ๋‹น์‹ ์ด <form action> ํ”„๋กญ์„ React18์—์„œ ์ถ”๊ฐ€ํ•œ๋‹ค๋ฉด ๋‹น์‹ ์€ ๊ฒฝ๊ณ (warning)๋ฅผ ๋ฐ›๊ฒŒ ๋  ๊ฒƒ์„ ๊ธฐ์–ตํ•˜๋ผ:

 

Warning: Invalid value for prop action on <form> tag. Either remove it from the element or pass a string or number value to keep it in the DOM.

 

๋‹ค์Œ๊ณผ ๊ฐ™์€ form์„ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋Š” React 19์—์„œ ์ด ๊ฒฝ๊ณ ๋Š” ๋” ์ด์ƒ ์‚ฌ์‹ค์ด ์•„๋‹ˆ๋‹ค.

 

import { useState } from 'react';

const AddToCartForm = ({ id, title, addToCart }) => {
  const formAction = async (formData) => {
    try {
      await addToCart(formData, title);
    } catch (e) {
      // show error notification
    }
  };

  return (
    <form action={formAction}>
      <h2>{title}</h2>
      <input type="hidden" name="itemID" value={id} />
      <button type="submit">Add to Cart</button>
    </form>
  );
};

type Item = {
  id: string;
  title: string;
};

const Cart = ({ cart }: { cart: Item[] }) => {
  if (cart.length == 0) {
    return null;
  }
  return (
    <>
      Cart content:
      <ul>
        {cart.map((item, index) => (
          <li key={index}>{item.title}</li>
        ))}
      </ul>
      <hr />
    </>
  );
};

export const App = () => {
  const [cart, setCart] = useState<Item[]>([]);

  const addToCart = async (formData: FormData, title) => {
    const id = String(formData.get('itemID'));
    // simulate an AJAX call
    await new Promise((resolve) => setTimeout(resolve, 1000));
    setCart((cart: Item[]) => [...cart, { id, title }]);

    return { id };
  };

  return (
    <>
      <Cart cart={cart} />
      <AddToCartForm
        id="1"
        title="JavaScript: The Definitive Guide"
        addToCart={addToCart}
      />
      <AddToCartForm
        id="2"
        title="JavaScript: The Good Parts"
        addToCart={addToCart}
      />
    </>
  );
};

 

accToCart ํ•จ์ˆ˜๋Š” ์„œ๋ฒ„ Action์ด ์•„๋‹ˆ๋‹ค. ์ด๊ฒƒ์€ ํด๋ผ์ด์–ธํŠธ ์ธก์—์„œ ํ˜ธ์ถœ๋˜๋ฉฐ ๋น„๋™๊ธฐ ๊ธฐ๋Šฅ์ผ ์ˆ˜ ์žˆ๋‹ค.

 

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋ฆฌ์•กํŠธ์—์„œ ๊ฒ€์ƒ‰ ํผ๊ณผ ๊ฐ™์€ AJAX ํผ์˜ ์ฒ˜๋ฆฌ๊ฐ€ ํฌ๊ฒŒ ๊ฐ„๋‹จํ•ด์งˆ ๊ฒƒ์ด๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์ด๊ฒƒ์€ ์–‘์‹ ์ œ์ถœ(์œ ํšจ์„ฑ ํ™•์ธ, ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ ๋“ฑ)์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ ์ด์ƒ์˜ ์ผ์„ ํ•˜๋Š” React Hook Form๊ณผ ๊ฐ™์€ ์„œ๋“œํŒŒํ‹ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์—†์•จ๋งŒํผ ์ถฉ๋ถ„ํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ๋‹ค. 

 

Tip: ๋‹น์‹ ์€ ์œ„ ์˜ˆ์ œ์—์„œ ๋ช‡๋ช‡ ์‚ฌ์šฉ์„ฑ ๋ฌธ์ œ๋ฅผ ๋ฐœ๊ฒฌํ•  ์ˆ˜ ์žˆ๋‹ค. (submit์„ ์ง„ํ–‰ํ•  ๋•Œ submit ๋ฒ„ํŠผ์ด ํ™œ์„ฑํ™”๋˜์ง€ ์•Š์Œ, ํ™•์ธ ๋ฉ”์„ธ์ง€ ๋ˆ„๋ฝ, ๋Šฆ์€ ์นดํŠธ ์—…๋ฐ์ดํŠธ). ๋‹คํ–‰ํžˆ๋„ ๋” ๋งŽ์€ ํ›…๋“ค์ด ์ด๋Ÿฌํ•œ ์‚ฌ์šฉ ์‚ฌ๋ก€๋ฅผ ๋•๊ธฐ ์œ„ํ•ด ์ œ๊ณต๋˜๊ณ  ์žˆ๋‹ค. ๊ณ„์† ์ฝ์–ด๋ณด๊ธธ!

 

 

React ๋ฌธ์„œ์—์„œ the <form action> prop์— ๋Œ€ํ•œ ์ž์„ธํ•œ ๋‚ด์šฉ์„ ์ฝ์–ด๋ณด์•„๋ผ.

 

 

useFormState

 

์ด ์ƒˆ๋กœ์šด ํ›…์€ ์•„๋ž˜ ๋ฌ˜์‚ฌํ•œ ๊ฒƒ์ฒ˜๋Ÿผ ๋น„๋™๊ธฐ ํผ ์•ก์…˜์„ ๋•๋Š” ์—ญํ• ์„ ํ•œ๋‹ค. useFormState๋ฅผ ํ˜ธ์ถœํ•ด ํผ์ด ์ œ์ถœ๋œ ๋งˆ์ง€๋ง‰ ์‹œ์ ๋ถ€ํ„ฐ ์•ก์…˜์˜ ๋ฐ˜ํ™˜ ๊ฐ’์— ์ ‘๊ทผํ•œ๋‹ค.

 

import { useFormState } from 'react-dom';
import { action } from './action';

function MyComponent() {
    const [state, formAction] = useFormState(action, null);
    // ...
    return <form action={formAction}>{/* ... */}</form>;
}

 

 

์˜ˆ๋ฅผ ๋“ค์–ด, ์ด๊ฒƒ์€ ๋‹น์‹ ์ด ํผ ์•ก์…˜์—์„œ ๋ฐ˜ํ™˜๋œ ํ™•์ธ ํ˜น์€ ์—๋Ÿฌ ๋ฉ”์„ธ์ง€๋ฅผ ๋ณด์—ฌ์ฃผ๊ฒŒ ํ•œ๋‹ค.

 

import { useState } from 'react';
import { useFormState } from 'react-dom';

const AddToCartForm = ({ id, title, addToCart }) => {
  const addToCartAction = async (prevState, formData) => {
    try {
      await addToCart(formData, title);
      return 'Added to cart';
    } catch (e) {
      return "Couldn't add to cart: the item is sold out.";
    }
  };

  const [message, formAction] = useFormState(addToCartAction, null);

  return (
    <form action={formAction}>
      <h2>{title}</h2>
      <input type="hidden" name="itemID" value={id} />
      <button type="submit">Add to Cart</button>&nbsp;
      {message}
    </form>
  );
};

type Item = {
  id: string;
  title: string;
};

export const App = () => {
  const [cart, setCart] = useState<Item[]>([]);

  const addToCart = async (formData: FormData, title) => {
    const id = String(formData.get('itemID'));
    // simulate an AJAX call
    await new Promise((resolve) => setTimeout(resolve, 1000));
    if (id === '1') {
      setCart((cart: Item[]) => [...cart, { id, title }]);
    } else {
      throw new Error('Unavailable');
    }

    return { id };
  };

  return (
    <>
      <AddToCartForm
        id="1"
        title="JavaScript: The Definitive Guide"
        addToCart={addToCart}
      />
      <AddToCartForm
        id="2"
        title="JavaScript: The Good Parts"
        addToCart={addToCart}
      />
    </>
  );
};

 

 

์ฃผ๋ชฉ: useFormState๋Š” react๊ฐ€ ์•„๋‹ˆ๋ผ react-dom์—์„œ ๋ถ€ํ„ฐ import๋˜์–ด์•ผ ํ•œ๋‹ค.

 

 

React ๋ฌธ์„œ์—์„œ useFormState์— ๋Œ€ํ•œ ์ž์„ธํ•œ ๋‚ด์šฉ์„ ์ฝ์–ด๋ณด์•„๋ผ.

 

 

 

useFormStatus

 

useFormStatus๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ถ€๋ชจ <form>์ด ํ˜„์žฌ ์ œ์ถœ ์ค‘์ด๊ฑฐ๋‚˜ ์„ฑ๊ณต์ ์œผ๋กœ ์ œ์ถœํ–ˆ๋Š”์ง€ ์•Œ ์ˆ˜ ์žˆ๋‹ค. ํผ์˜ ์ž์‹์—์„œ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ ๋‹ค์Œ ์†์„ฑ์„ ๊ฐ€์ง„ ๊ฐœ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค:

 

const { pending, data, method, action } = useFormStatus();

 

 

๋‹น์‹ ์€ ๋ฐ์ดํ„ฐ ์†์„ฑ์„ ํ†ตํ•ด ์œ ์ €๊ฐ€ ์ œ์ถœ ์ค‘์ธ ๋ฐ์ดํ„ฐ๋ฅผ ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ๋‹ค. ํผ์ด ์ œ์ถœ๋˜๋Š” ๋™์•ˆ ๋ฒ„ํŠผ์ด ๋น„ํ™œ์„ฑํ™”๋œ pending ์ƒํƒœ ๋˜ํ•œ ์•„๋ž˜ ์˜ˆ์‹œ์™€ ๊ฐ™์ด ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ๋‹ค:

 

import { useState } from 'react';
import { useFormStatus } from 'react-dom';

const AddToCartForm = ({ id, title, addToCart }) => {
  const formAction = async (formData) => {
    try {
      await addToCart(formData, title);
    } catch (e) {
      // show error notification
    }
  };

  return (
    <form action={formAction}>
      <h2>{title}</h2>
      <input type="hidden" name="itemID" value={id} />
      <SubmitButton />
    </form>
  );
};

const SubmitButton = () => {
  const { pending } = useFormStatus();
  return (
    <button disabled={pending} type="submit">
      Add to Cart
    </button>
  );
};

type Item = {
  id: string;
  title: string;
};

const Cart = ({ cart }: { cart: Item[] }) => {
  if (cart.length == 0) {
    return null;
  }
  return (
    <>
      Cart content:
      <ul>
        {cart.map((item, index) => (
          <li key={index}>{item.title}</li>
        ))}
      </ul>
      <hr />
    </>
  );
};

export const App = () => {
  const [cart, setCart] = useState<Item[]>([]);

  const addToCart = async (formData: FormData, title) => {
    const id = String(formData.get('itemID'));
    // simulate an AJAX call
    await new Promise((resolve) => setTimeout(resolve, 1000));
    setCart((cart: Item[]) => [...cart, { id, title }]);

    return { id };
  };

  return (
    <>
      <Cart cart={cart} />
      <AddToCartForm
        id="1"
        title="JavaScript: The Definitive Guide"
        addToCart={addToCart}
      />
      <AddToCartForm
        id="2"
        title="JavaScript: The Good Parts"
        addToCart={addToCart}
      />
    </>
  );
};

 

 

์ฃผ๋ชฉ: useFormStatus๋Š” react๊ฐ€ ์•„๋‹Œ react-dom์—์„œ importํ•ด์•ผ ํ•œ๋‹ค. ๋˜ํ•œ ๋ถ€๋ชจ ํผ์ด ์œ„์—์„œ ์„ค๋ช…ํ•œ action prop์„ ์‚ฌ์šฉํ•  ๋•Œ๋งŒ ์ž‘๋™ํ•œ๋‹ค.

 

์ด ํ›…์€ useFormState์™€ ํ•จ๊ป˜ ๋ถˆํ•„์š”ํ•œ ์ปจํ…์ŠคํŠธ ํšจ๊ณผ๋กœ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์–ด์ˆ˜์„ ํ•˜๊ฒŒ ๋งŒ๋“ค์ง€ ์•Š๊ณ  ํด๋ผ์ด์–ธํŠธ ์ธก ํผ์˜ ์œ ์ € ๊ฒฝํ—˜์„ ํ–ฅ์ƒ์‹œํ‚ฌ ๊ฒƒ์ด๋‹ค. useFormStatus ํ›„ํฌ์— ๋Œ€ํ•œ ์ž์„ธํ•œ ๋‚ด์šฉ์€ React ๋ฌธ์„œ์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

 

 

useOptimistic

 

์ด ์ƒˆ๋กœ์šด ํ›…์€ actioin์ด ์ œ์ถœ๋˜๋Š” ๋™์•ˆ ์‚ฌ์šฉ์ž ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ตœ์ ์œผ๋กœ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•œ๋‹ค. 

 

 

import { useOptimistic } from 'react';

function AppContainer() {
    const [optimisticState, addOptimistic] = useOptimistic(
        state,
        // updateFn
        (currentState, optimisticValue) => {
            // merge and return new state
            // with optimistic value
        },
    );
}

 

 

์œ„ ์นดํŠธ ์˜ˆ์ œ์—์„œ ์šฐ๋ฆฌ๋Š” AJAX ํ˜ธ์ถœ์ด ์™„๋ฃŒ๋˜๊ธฐ ์ „์— ์ƒˆ ํ•ญ๋ชฉ์ด ์ถ”๊ฐ€๋œ ์นดํŠธ๋ฅผ ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ๋‹ค: 

 

import { useState, useOptimistic } from 'react';

const AddToCartForm = ({ id, title, addToCart, optimisticAddToCart }) => {
  const formAction = async (formData) => {
    optimisticAddToCart({ id, title });
    try {
      await addToCart(formData, title);
    } catch (e) {
      // show error notification
    }
  };

  return (
    <form action={formAction}>
      <h2>{title}</h2>
      <input type="hidden" name="itemID" value={id} />
      <button type="submit">Add to Cart</button>
    </form>
  );
};

type Item = {
  id: string;
  title: string;
};

const Cart = ({ cart }: { cart: Item[] }) => {
  if (cart.length == 0) {
    return null;
  }
  return (
    <>
      Cart content:
      <ul>
        {cart.map((item, index) => (
          <li key={index}>{item.title}</li>
        ))}
      </ul>
      <hr />
    </>
  );
};

export const App = () => {
  const [cart, setCart] = useState<Item[]>([]);

  const [optimisticCart, optimisticAddToCart] = useOptimistic<Item[], Item>(
    cart,
    (state, item) => [...state, item]
  );

  const addToCart = async (formData: FormData, title) => {
    const id = String(formData.get('itemID'));
    // simulate an AJAX call
    await new Promise((resolve) => setTimeout(resolve, 1000));
    setCart((cart: Item[]) => [...cart, { id, title }]);

    return { id };
  };

  return (
    <>
      <Cart cart={optimisticCart} />
      <AddToCartForm
        id="1"
        title="JavaScript: The Definitive Guide"
        addToCart={addToCart}
        optimisticAddToCart={optimisticAddToCart}
      />
      <AddToCartForm
        id="2"
        title="JavaScript: The Good Parts"
        addToCart={addToCart}
        optimisticAddToCart={optimisticAddToCart}
      />
    </>
  );
};

 

 

UI๋ฅผ ์ตœ์ ์œผ๋กœ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๊ฒƒ์€ ์›น ์•ฑ์˜ ์œ ์ € ๊ฒฝํ—˜์„ ํ–ฅ์ƒ์‹œํ‚ค๋Š” ์ข‹์€ ๋ฐฉ๋ฒ•์ด๋‹ค. ์ด ํ›…์€ ์ด ์‚ฌ์šฉ ์‚ฌ๋ก€์— ๋งŽ์€ ๋„์›€์ด ๋œ๋‹ค. useOptimistic์— ๋Œ€ํ•œ ์ž์„ธํ•œ ๋‚ด์šฉ์€ React ๋ฌธ์„œ์—์„œ ์ฝ์„ ์ˆ˜ ์žˆ๋‹ค.

 

 

๋ณด๋„ˆ์Šค: ๋น„๋™๊ธฐ ์ „ํ™˜

React์˜ Transition API๋Š” UI ์ฐจ๋‹จ์—†์ด ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•œ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์‚ฌ์šฉ์ž๊ฐ€ ๋งˆ์Œ์„ ๋ฐ”๊ฟ€ ๊ฒฝ์šฐ ์ด์ „ ์ƒํƒœ ๋ณ€๊ฒฝ์„ ์ทจ์†Œํ•  ์ˆ˜ ์žˆ๋‹ค.

 

์ด ์•„์ด๋””์–ด๋Š” startTransition๋ฅผ ํ˜ธ์ถœํ•ด ์ƒํƒœ ๋ณ€ํ™”๋ฅผ ๊ฐ์‹ผ๋‹ค.

 

function TabContainer() {
    const [isPending, startTransition] = useTransition();
    const [tab, setTab] = useState('about');

    function selectTab(nextTab) {
        // instead of setTab(nextTab), put the state change in a transition
        startTransition(() => {
            setTab(nextTab);
        });
    }
    // ...
}

 

๋‹ค์Œ ์˜ˆ๋Š” ์ด Transition API๋ฅผ ์‚ฌ์šฉํ•œ ํƒญ ๋„ค๋น„๊ฒŒ์ด์…˜์„ ๋ณด์—ฌ์ค€๋‹ค. "Posts"์„ ํด๋ฆญํ•œ ๋‹ค์Œ ์ฆ‰์‹œ "Contact"๋ฅผ ํด๋ฆญํ•œ๋‹ค. ์ด๋กœ ์ธํ•ด "Posts"์˜ ๋А๋ฆฐ ๋ Œ๋”๋ง์ด ์ค‘๋‹จ๋œ๋‹ค. "Contact" ํƒญ์ด ์ฆ‰์‹œ ํ‘œ์‹œ๋œ๋‹ค. ์ด ์ƒํƒœ ์—…๋ฐ์ดํŠธ๋Š” ํŠธ๋žœ์ง€์…˜์œผ๋กœ ํ‘œ์‹œ๋˜๊ธฐ ๋•Œ๋ฌธ์— ๋А๋ฆฐ ๋ฆฌ๋ Œ๋”(re-render)๋Š” ์‚ฌ์šฉ์ž ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ํ†ต์ œํ•˜์ง€ ์•Š๋Š”๋‹ค.

 

import { useState, useTransition } from 'react';
import TabButton from './TabButton';
import AboutTab from './AboutTab';
import PostsTab from './PostsTab';
import ContactTab from './ContactTab';

export function App() {
  const [isPending, startTransition] = useTransition();
  const [tab, setTab] = useState('about');

  function selectTab(nextTab) {
    startTransition(() => {
      setTab(nextTab);
    });
  }

  return (
    <>
      <TabButton isActive={tab === 'about'} onClick={() => selectTab('about')}>
        About
      </TabButton>
      <TabButton isActive={tab === 'posts'} onClick={() => selectTab('posts')}>
        Posts (slow)
      </TabButton>
      <TabButton
        isActive={tab === 'contact'}
        onClick={() => selectTab('contact')}
      >
        Contact
      </TabButton>
      <hr />
      {tab === 'about' && <AboutTab />}
      {tab === 'posts' && <PostsTab />}
      {tab === 'contact' && <ContactTab />}
    </>
  );
}

 

 

React 18.2์—์„œ๋Š” Transition ํ›…์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. React 19์˜ ์ƒˆ๋กœ์šด ์ ์€ ์ด์ œ ๋น„๋™๊ธฐ ๊ธฐ๋Šฅ์„ ์ง€๋‚˜ Transition์„ ์‹œ์ž‘ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, React๋Š” Transition์„ ์‹œ์ž‘ํ•˜๊ธฐ๋ฅผ ๊ธฐ๋‹ค๋ฆฌ๊ณ  ์žˆ๋‹ค.

 

์ด๋Š” AJAX ํ˜ธ์ถœ์„ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ์ œ์ถœํ•˜๊ณ  ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ํŠธ๋žœ์ง€์…˜์œผ๋กœ ๋ Œ๋”๋งํ•˜๋Š” ๋ฐ ์œ ์šฉํ•˜๋‹ค. ํŠธ๋žœ์ง€์…˜์˜ pending ์ƒํƒœ๋Š” ๋น„๋™๊ธฐ ๋ฐ์ดํ„ฐ ์ œ์ถœ๊ณผ ํ•จ๊ป˜ ์‹œ์ž‘๋œ๋‹ค. ์ด๋Š” ์œ„์—์„œ ์„ค๋ช…ํ•œ ํผ actions ๊ธฐ๋Šฅ์—์„œ ์ด๋ฏธ ์‚ฌ์šฉ๋˜๊ณ  ์žˆ๋‹ค. React๊ฐ€ startTransition์œผ๋กœ ๊ฐ์‹ธ์ง„ <form action> ํ•ธ๋“ค๋Ÿฌ๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ํ˜„์žฌ ํŽ˜์ด์ง€๋ฅผ ์ฐจ๋‹จํ•˜์ง€ ์•Š๋Š”๋‹ค.

 

 

์ด ๊ธฐ๋Šฅ์€ ์•„์ง React ์„ค๋ช…์„œ์— ๋ฌธ์„œํ™”๋˜์–ด ์žˆ์ง€ ์•Š์ง€๋งŒ pull request์—์„œ ์ด ๊ธฐ๋Šฅ์— ๋Œ€ํ•œ ์ž์„ธํ•œ ๋‚ด์šฉ์„ ์ฝ์„ ์ˆ˜ ์žˆ๋‹ค.

 

๊ฒฐ๋ก 

์ด๋Ÿฌํ•œ ๋ชจ๋“  ๊ธฐ๋Šฅ๋“ค์€ ํด๋ผ์ด์–ธํŠธ ์ „์šฉ์ธ ๋ฆฌ์•กํŠธ ์•ฑ์—์„œ ์ž‘๋™ํ•˜๋ฉฐ, Vite์™€ ํ•จ๊ป˜ ๋ฒˆ๋“ค๋œ ์•ฑ์—์„œ ์ž‘๋™ํ•œ๋‹ค. ๋น„๋ก ์„œ๋ฒ„ ํ†ตํ•ฉํ˜• ๋ฆฌ์•กํŠธ ์•ฑ์—์„œ๋„ ์ด๊ฒƒ์ด ์ž‘๋™ํ•˜์ง€๋งŒ ์ด๊ฒƒ์„ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด Next ํ˜น์€ Remix์™€ ๊ฐ™์€ SSR ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์‚ฌ์šฉํ•  ํ•„์š”๋Š” ์—†๋‹ค. 

 

์ด๋Ÿฌํ•œ ๊ธฐ๋Šฅ์„ ํ†ตํ•ด ๋ฆฌ์•กํŠธ์—์„œ ๋ฐ์ดํ„ฐ ํŒจ์นญ๊ณผ ํผ์„ ๊ตฌํ˜„ํ•˜๊ธฐ๊ฐ€ ํ›จ์”ฌ ์‰ฌ์›Œ์ง„๋‹ค. ํ•˜์ง€๋งŒ ํ›Œ๋ฅญํ•œ ์œ ์ € ๊ฒฝํ—˜์„ ๋งŒ๋“ค๋ ค๋ฉด ์ด ๋ชจ๋“  ํ›…๋ฅผ ํ†ตํ•ฉํ•ด์•ผ ํ•˜๋Š”๋ฐ, ์ด๋Š” ๋ณต์žกํ•  ์ˆ˜ ์žˆ๋‹ค. ๋˜๋Š” ์ตœ์ ์˜ ๋นŒํŠธ์ธ ์—…๋ฐ์ดํŠธ๋ฅผ ์ง€์›ํ•˜๋Š” ์‚ฌ์šฉ์ž ์นœํ™”์ ์ธ ํผ์„ ๊ฐ€์ง„ react-admin๊ณผ ๊ฐ™์€ ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

 

์ด ๊ธฐ๋Šฅ๋“ค์€ ์™œ React 18.3์ด ์•„๋‹Œ React 19์— ๋„์ž…๋˜๋Š” ๊ฒƒ์ผ๊นŒ? ์ด ๊ธฐ๋Šฅ๋“ค์—๋Š” ์•ฝ๊ฐ„์˜ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์ด ํฌํ•จ๋˜์–ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์— 18.3 release์—๋Š” ์—†์„ ๊ฒƒ์œผ๋กœ ๋ณด์ธ๋‹ค.

 

React 19๋Š” ์–ธ์ œ ์˜ฌ ๊ฒƒ์ธ๊ฐ€ ? ์•„์ง ETA๋Š” ์—†์ง€๋งŒ ์ด ๊ฒŒ์‹œ๋ฌผ์— ์–ธ๊ธ‰๋œ ๋ชจ๋“  ๊ธฐ๋Šฅ์€ ์ด๋ฏธ ์ž‘๋™ํ•˜๊ณ  ์žˆ๋‹ค. ํ•˜์ง€๋งŒ ์•„์ง ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ์ถ”์ฒœํ•˜๊ณ  ์‹ถ์ง€๋Š” ์•Š๋‹ค. (Next.js๊ฐ€ ๊ทธ๋ ‡๊ฒŒ ํ•œ๋‹ค๊ณ  ํ•ด๋„) canary release๋ฅผ ํ”„๋กœ๋•ํŠธ์— ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์€ ์ข‹์€ ์ƒ๊ฐ์ด ์•„๋‹ˆ๋‹ค.

 

๋ฆฌ์•กํŠธ ์ฝ”์–ด ํŒ€์ด SSR ์•ฑ์—์„œ ์ผํ•˜๋Š” ์‚ฌ๋žŒ๋“ค๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ๋ชจ๋“  ๋ฆฌ์•กํŠธ ๊ฐœ๋ฐœ์ž๋“ค์˜ ๊ฐœ๋ฐœ์ž ๊ฒฝํ—˜์„ ๊ฐœ์„ ํ•˜๊ธฐ ์œ„ํ•ด ๋…ธ๋ ฅํ•˜๊ณ  ์žˆ๋‹ค๋Š” ๊ฒƒ์€ ์ •๋ง ๊ธฐ์œ ์ผ์ด๋‹ค. ๋˜ํ•œ ๊ทธ๋“ค์€ ์ปค๋ฎค๋‹ˆํ‹ฐ ํ”ผ๋“œ๋ฐฑ์— ๊ท€๋ฅผ ๊ธฐ์šธ์ด๊ณ  ์žˆ๋Š” ๊ฒƒ ๊ฐ™๋‹ค - ๋ฐ์ดํ„ฐ ํŒจ์นญ ๋ฐ ํผ ํ•ธ๋“ค๋ง์€ ๋งค์šฐ ํ”ํ•œ ๋ฌธ์ œ์ด๋‹ค.

 

๋‚˜๋Š” React์˜ Stable ๋ฆด๋ฆฌ์ฆˆ์— ์ด ๊ธฐ๋Šฅ๋“ค์ด ๋ณด์ด๊ธธ ๊ธฐ๋Œ€ํ•œ๋‹ค!

 

 

๐Ÿ„‍โ™€๏ธ input ํƒœ๊ทธ๋กœ ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•˜๊ฑฐ๋‚˜ ํŒŒ์ผ์˜ url์„ ๊ฐ€์ง€๊ณ  ์žˆ์„ ๊ฒฝ์šฐ ํŒŒ์ผ์˜ ํ™•์žฅ์ž๋ฅผ ์ œ๋Œ€๋กœ ์•Œ ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์•Œ์•„๋ณด์ž.

 

 

How to: ํŒŒ์ผ ์—…๋กœ๋“œ ํ›„ e.target.files์˜ name์„ ํ™œ์šฉ

<input type="file" id="avatar" name="avatar" accept="*" />

 

 

 

 

์œ„ ํŒŒ์ผ ๊ฐ์ฒด๋Š” inputํƒœ๊ทธ๋ฅผ ํ†ตํ•ด ํŒŒ์ผ์„ ์—…๋กœ๋“œ ํ•œ ํ›„ ์–ป๊ฒŒ ๋˜๋Š” ๊ฐ’์ด๋‹ค.

 

๊ทธ ์ค‘ name๊ฐ’์—๋Š” ํŒŒ์ผ์˜ ์ด๋ฆ„์ด ์กด์žฌํ•˜๋Š”๋ฐ ์ด๋ฅผ ํ™œ์šฉํ•ด ํ™•์žฅ์ž ๊ฐ’์„ ๊ตฌํ•  ์ˆ˜ ์žˆ๋‹ค.

 

<input
    type="file"
    id="upload"
    onChange={(e) => {
      const files = e.target.files;
      const filename = files[0].name;
      const splitedNameArr = filename.split('.');
      const lastSplitedVal = splitedNameArr.at(-1);

      console.log(lastSplitedVal);
    }}
  />

 

ํŒŒ์ผ name์—์„œ split์„ ํ†ตํ•ด . ์„ ๊ธฐ์ค€์œผ๋กœ ๋‚˜๋ˆˆ ๋’ค ๋งˆ์ง€๋ง‰ . ๋’ค์˜ ๊ฐ’์„ ํ™•์žฅ์ž๋กœ ์ธ์‹ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ํ™œ์šฉํ–ˆ๋‹ค.

 

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ํŒŒ์ผ ์ด๋ฆ„ ์‚ฌ์ด์— . ์ด ์กด์žฌํ•˜๋”๋ผ๋„ ๊ฐ€์žฅ ๋งˆ์ง€๋ง‰ . ๋’ค ๊ฐ’์„ ํ™•์žฅ์ž๋กœ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

 

 

* Array.prototype.at() ๊ธฐ๋Šฅ์ด ๋„ˆ๋ฌด ๋ง˜์— ๋“ค์–ด ๋งํฌ ๋‚จ๊ธด๋‹ค!

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/at

+ Recent posts