
๐ useEffect๋ useMemo๋ ๋ต์ด ์๋์๋ค — ํ ๋ฐ์ ๋ฆ๋ state
๋ค์ด๊ฐ๋ฉฐ
ํด์ธ T&A ์ํ ์์ธ(PDP) ํ์ด์ง์๋ ๋ ๊ฐ์ ์์ฝ ๋ฒํผ์ด ์๋ค. ํ๋๋ ์ต์ ์์ญ ์์ ์์ฝ ๋ฒํผ์ด๊ณ , ๋ค๋ฅธ ํ๋๋ ์คํฌ๋กค์ ๋ด๋ฆฌ๋ฉด ๋ฐ๋ผ๋ค๋๋ CTA ํ๋กํ ๋ฒํผ์ด๋ค.
๋งก๋ ์ ๋ฌด๊ฐ ์์๋ณด๋ค ๋น ๋ฅด๊ฒ ๋ง๋ฌด๋ฆฌ ๋์ด ๋ฆฌ์์ค๊ฐ ์กฐ๊ธ ๋จ์ ๋ฐฑ๋ก๊ทธ๋ฅผ ์ดํด๋ณด๋ค ๋์ ๋๋ ํฐ์ผ์ ๋ฐ๊ฒฌํ๋ค.
ํ์ ์ต์ ์ ์ ํํ์ง ์์๋๋ฐ๋ CTA ํ๋กํ "์์ฝํ๊ธฐ"๋ฅผ ๋๋ฅด๋ฉด ๊ฒฐ์ ํ์ด์ง๋ก ๋์ด๊ฐ๋ค.
์ต์ ์์ญ ๋ฒํผ์ ๊ฒ์ฆ์ ํต๊ณผํ์ง ๋ชป ํ๋ฉด ๋ฒํผ์ด disabled ์ฒ๋ฆฌ๊ฐ ๋๋๋ฐ, CTA ํ๋กํ ๋ฒํผ์ ํด๋ฆญ์ด ๊ฐ๋ฅํ ์ํ์๋ค. ๋ ๋ฒํผ์ด ๊ฐ์ ์ํ๋ฅผ ๊ณต์ ํ์ง ๋ชปํ๊ณ ์๋ ๊ฒ์ด๋ค.
๋ฌธ์ ๋ฐ๊ฒฌ
๋ฒํผ disabled ์ฒ๋ฆฌ๋ ์ต์
์ปดํฌ๋ํธ(/item/index.tsx)์์ ๊ณ์ฐํ๋ค. ์ ์ ๊ฐ ์ ํํ ์๋ (finalAmounts), ํ์์ฌ๋กฏ (selectedTimeslot), ์ํ์์ธ ๋ฐ์ดํฐ๋ก๋ถํฐ ํ์ ์ต์
, ๋จ๋
๊ตฌ๋งค, ์ต์ ์ธ์ ์กฐ๊ฑด์ ๊ณ ๋ คํ์ฌ ๋ง๋ ๊ฐ์ด๋ค.
๋ฌธ์ ๋ CTA ํ๋กํ
๋ฒํผ์ด ๋ณ๋ ์ปดํฌ๋ํธ(/cta/desktop.tsx)๋ผ๋ ๊ฒ์ธ๋ฐ, ์ด ๊ณ์ฐ ๋ก์ง์ ๊ทธ๋๋ก ๊ฐ์ ธ๊ฐ๋ฉด CTA ๋ฒํผ์ด ์ํ ๋ฐ์ดํฐ์ ๊ฒ์ฆ ๋ก์ง ์ ์ฒด๋ฅผ ๊ฐ์ง๊ฒ ๋์ด ๊ฒฐํฉ๋๊ฐ ๋๋ฌด ๋์์ง๋ค. ๊ทธ๋์ ๊ณ์ฐ์ ์ต์
์ปดํฌ๋ํธ์์ ์งํํ๊ณ , ๊ฒฐ๊ณผ ๊ฐ๋ง ์ ์ญ store์ ์ค์ด์ CTA๊ฐ ์ฝ๋๋ก ํ๊ธฐ๋ก ํ๋ค.
// floatingOptionsAtom ์ buttonDisabled ํ๋ ์ถ๊ฐ
export const floatingOptionsAtom = atom<{
// ...
buttonDisabled?: boolean
} | undefined>(undefined)
// cta/desktop.tsx — ์ฝ์ด์ disabled ์ ๋ฐ์
const floatingOptionButtonDisabled = floatingOptions && floatingOptions.buttonDisabled
return (<CtaButton disabled={!isSaleable || floatingOptionButtonDisabled} ... />)
์ฌ๊ธฐ๊น์ง๋ ๋จ์ํด ๋ณด์๋ค. ๋ฌธ์ ๋ ์ต์ ์ปดํฌ๋ํธ๊ฐ atom์ buttonDisabled๋ฅผ ์ธ์ , ์ด๋ค ๊ฐ์ผ๋ก ๋ฃ๋๋์๋ค.
๐ ์๋ 1 — useEffect๋ก atom์ ๋๊ธฐํ
๊ฐ์ฅ ๋น ๋ฅด๊ฒ ์ฒ๋ฆฌํ ์ ์๋ ๋ฐฉ๋ฒ. buttonDisabled๊ฐ ๋ณ๊ฒฝ๋ ๋๋ง๋ค useEffect๋ก atom์ ๋ฐ๋ผ ์ฐ๊ฒ ํ๋ค.
const buttonDisabled = useMemo(() => compute(finalAmounts, selectedTimeslot), [finalAmounts, selectedTimeslot])
useEffect(() => {
setFloatingOptions((prev) => (prev && { ...prev, buttonDisabled }))
}, [buttonDisabled])
๊ฒฐ๊ณผ๋ ํ ๋ฐ์ (1๋ ๋) ๋ฆ๊ฒ ๋ฐ์๋จ. ์ต์ ์ ์ ํํ ์งํ CTA๋ฅผ ๋๋ฅด๋ฉด ๋นํ์ฑํ๋ผ์ผ ํ ๋ฒํผ์ด ๊ทธ๋๋ก ๋๋ ธ๋ค.
์์ธ์ useEffect์ ์คํ ์์ ์ด๋ค. useEffect๋ ๋ ๋๊ฐ ํ๋ฉด์ ์ปค๋ฐ๋ ์ดํ์ ์คํ๋๋ค.
- ์ต์
๋ณ๊ฒฝ →
setFinalAmounts(์ ๊ฐ) - ๋ฆฌ๋ ๋ → useMemo๊ฐ
buttonDisabled์ฌ๊ณ์ฐ - DOM ์ปค๋ฐ
- ๊ทธ์ ์์ผ useEffect ์คํ →
setFloatingOptions(buttonDisabled ๋ฎ์ด์ฐ๊ธฐ) - 4๋ฒ ๋๋ฌธ์ ๋ค์ ํ ๋ฒ ๋ ๋ ๋
buttonDisabled๊ฐ atom์ ๋๋ฌํ๋๊ฑด 4๋ฒ ์์ ์ธ๋ฐ, ๊ทธ ์ (2 ~ 3๋ฒ ์ฌ์ด)์ ํด๋ฆญ์ด ๋ค์ด์ค๋ฉด ์ด์ ์ฌ์ดํด ๊ฐ์ด ์กํ๋ค.
๐ ์๋ 2 — useEffect ๋์ setFloatingOptions์ ์ง์ ๋ฎ์ด์ฐ๊ธฐ
๊ทธ๋ฌ๋ฉด, useEffect๋ฅผ ๋นผ๊ณ , ์ต์ ์ ๋ฐ๊พธ๋ ํธ๋ค๋ฌ์์ setFloatingOptions์ buttonDisabled๋ฅผ ๋ฐ๋ก ๋ฃ์ผ๋ฉด ๋์ง ์์๊น?
onChange={(value) => {
const amounts = amountToSnapshotOptions({ value, ... })
setFinalAmounts(amounts)
setFloatingOptions({ ...floatingOptions, amounts, buttonDisabled })
// ^^^^^^^^^^^^^^ useMemo ๊ฐ
}}
๊ทธ๋ฐ๋ฐ ์ฌ์ ํ ํ ๋ฐ์๊ฐ ๋ฆ์๋ค. ์ฌ๊ธฐ์ ํค๋งธ๋๋ฐ, ํต์ฌ ์ธ์ฌ์ดํธ๋ ์ด๊ฑฐ์๋ค.
๋ ๋ ์ค ๊ณ์ฐ๋ ๊ฐ (useMemo๋ ์๋๋ ) ์ง์ ์ ์ปค๋ฐ๋ state ๊ธฐ์ค์ด๋ค.
์ด ํธ๋ค๋ฌ ์์์ ์ฐธ์กฐํ๋ buttonDisabled๋ ์ด๋ฒ ๋ ๋์ useMemo์ ๊ฒฐ๊ณผ = ์ง์ finalAmounts ๊ธฐ์ค ๊ฐ์ด๋ค. ๋ฐฉ๊ธ setFinalAmounts(amounts)๋ฅผ ํธ์ถํ์ด๋ ๊ทธ๊ฑด ๋ค์ ๋ ๋๋ฅผ ์์ฝํ ๊ฒ์ผ ๋ฟ, ์ง๊ธ ํธ๋ค๋ฌ๊ฐ ๋ณด๋ buttonDisabled๋ ์ amounts๋ฅผ ๋ชจ๋ฅธ๋ค.
useMemo๋ ๋ค์ ๋ ๋์์์ผ ์ฌ๊ณ์ฐ๋๊ธฐ ๋๋ฌธ์ด๋ค. ์ฆ useEffect๋ฅผ ์์ ๋, staleํ useMemo๋ฅผ ๊ทธ๋๋ก ๋ฃ๋ ํ ๋ฌด์กฐ๊ฑด ๋ฆ๋๋ค. useMemo๋ฅผ ๋นผ๋ ๋๊ฐ๋ค. "useMemo๊ฐ ์บ์ฑํด์ ๋ฆ๋ ๊ฑด๊ฐ?" ์ถ์ด์ useMemo๋ฅผ ๋นผ๊ณ ๋ ๋ ๋ณธ๋ฌธ์์ ๊ทธ๋ฅ ๊ณ์ฐํด๋ดค๋ค.
// useMemo ์ ๊ฑฐ — ๋ ๋ํ ๋๋ง๋ค ์ง์ ๊ณ์ฐ
const buttonDisabled = getItemButtonDisabled({ detailData, finalAmounts, selectedTimeslot })
onChange={(value) => {
const amounts = amountToSnapshotOptions({ value, ... })
setFinalAmounts(amounts)
setFloatingOptions({ ...floatingOptions, amounts, buttonDisabled })
// ^^^^^^^^^^^^^^ ์ฌ์ ํ ์ ๊ฐ
}}
๊ทธ๋๋ ํ ๋ฐ์ ๋ฆ์๋ค. ์ด์ ๋ ํด๋ก์ ์ ์๋ค. onChange ํธ๋ค๋ฌ๋ ์ด๋ฒ ๋ ๋์์ ๋ง๋ค์ด์ง ํจ์๋ผ, ๊ทธ ์์ buttonDisabled๋ ์ด๋ฒ ๋ ๋ ์์ ์ finalAmounts(์ ๊ฐ)๋ก ๊ณ์ฐ๋ ๊ฐ์ ์บก์ฒํ๋ค.
ํธ๋ค๋ฌ ์์์ setFinalAmounts(amounts)๋ฅผ ๋ถ๋ฌ๋ ๊ทธ๊ฑด ๋ค์ ๋ ๋๋ฅผ ์์ฝํ ๋ฟ, ์ด๋ฏธ ์บก์ฒ๋ buttonDisabled๋ ๋ฐ๋์ง ์๋๋ค.
์ฆ ๋ฆ๋ ์์ธ์ useMemo์ ์บ์ฑ์ด ์๋๋ผ, "๋ ๋ ์์ ์ ๋ง๋ค์ด์ง ๊ฐ์, ๊ทธ ๋ ๋์์ ์์ฑ๋ ํธ๋ค๋ฌ๊ฐ ๊ทธ๋๋ก ๋ค๊ณ ์๋ค"๋ ๊ตฌ์กฐ ์์ฒด์๋ค. memoize๋ ๊ฑฐ๋ค ๋ฟ, ๋นผ๋ ๊ฒฐ๊ณผ๋ ๊ฐ๋ค.
๊ทธ๋์ ๋ต์ ํ๋๋ก ๋ชจ์ธ๋ค — ๋ ๋๊ฐ ๋ง๋ค์ด ์ค ๊ฐ์ ์ฌ์ฌ์ฉํ์ง ๋ง๊ณ , ํธ๋ค๋ฌ๊ฐ ๋ฐฉ๊ธ ๋ง๋ amounts๋ก ๊ทธ ์๋ฆฌ์์ ๋ค์ ๊ณ์ฐํ ๊ฒ.
๐ก react 18์ ์ฌ์ฉํ๋ useTransition, flushSync๋?
- useTransition / useDeferredValue๋ ์ ๋ฐ์ดํธ๋ฅผ ๋ฏธ๋ฃจ๋(defer) API๋ค. "๊ฐ์ cycle ๋ด ์ฆ์ ๋ฐ์"๊ณผ๋ ์ ๋ฐ๋๋ผ ๋์์ด ์ ๋๋ค.
- flushSync๋ก ๊ฐ์ ๋๊ธฐ ๋ ๋๋ฅผ ์ํค๋ฉด ์ฆ์์ ๊ฐ๋ ค์ง์ง๋ง ๊ทผ๋ณธ์ ์ธ ํด๊ฒฐ๋ฐฉ์์ ์๋๋ค. React 18์ ์๋ ๋ฐฐ์นญ์ ๊นจ์ ์ฑ๋ฅ์ ๋จ์ด๋จ๋ฆฌ๊ณ , ๋ฆ๋ ์์ธ(๋ ๋ ํ์๊ฐ์ ์ง์ ์ปค๋ฐ state ๊ธฐ์ค)์ ๊ทธ๋๋ก๋ผ ์กฐ๊ฑด์ด ์ถ๊ฐ๋๊ธฐ๋ผ๋ ํ๋ค๋ฉด ์ํ๊ฐ ๊นจ์ง๊ธฐ๋ ์ฝ๋ค.
์ฆ, ํ์ด๋ฐ API๋ก ํ ๋ฌธ์ ๊ฐ ์๋๋ผ, ๋ฐ์ดํฐ ํ๋ฆ์ ๊ณ ์ณ์ผ ํ๋ ๋ฌธ์ ์๋ค.
๐ ์ต์ข ์๋ฃจ์ — ์ฐ๋ ์์ ์ fresh ๊ฐ์ผ๋ก ์ง์ ๊ณ์ฐ
ํด๊ฒฐ์ ์์ธ๋ก ๋จ์ํ๋ค. useMemo๊ฐ ๋ค์ ๋ ๋์ ๊ณ์ฐํด ์ค ๊ฐ์, ํธ๋ค๋ฌ๊ฐ ์ด๋ฏธ ์์ ์ฅ fresh ๊ฐ(amounts)๋ก ๋ฏธ๋ฆฌ ๋น๊ฒจ์ ์ง์ ๊ณ์ฐํด ๊ฐ์ ํธ๋์ญ์
์ ํจ๊ป ๋ฃ์ผ๋ฉด ๋๋ค.
๋จผ์ ๊ณ์ฐ ๋ก์ง์ ์์ ํจ์๋ก ์ถ์ถํด useMemo์ ํธ๋ค๋ฌ๊ฐ ๊ฐ์ ๊ณต์์ ๊ณต์ ํ๊ฒ ํ๋ค.
// helpers.ts
export function getItemButtonDisabled({ detailData, finalAmounts, selectedTimeslot }): boolean {
if (!detailData) return true
// ... ํ์ ์ต์
/ ๋จ๋
๊ตฌ๋งค / ์ต์ ์ธ์ / ํ์ฐจ ์กฐ๊ฑด ๊ณ์ฐ
return hasTimeslots ? !(!!selectedTimeslot && isCommonConditions) : !isCommonConditions
}
๊ทธ๋ฆฌ๊ณ atom์ ์ฐ๋ ์์ ์ fresh ๊ฐ์ผ๋ก ํธ์ถํ๋ค.
onChange={(value) => {
const amounts = amountToSnapshotOptions({ value, ... })
const buttonDisabled = getItemButtonDisabled({
detailData,
finalAmounts: amounts, // โ
state๊ฐ ์๋๋ผ ๋ฐฉ๊ธ ๋ง๋ fresh ๊ฐ
selectedTimeslot,
})
setFinalAmounts(amounts)
setFloatingOptions({ ...floatingOptions, amounts, buttonDisabled })
}}
ํ์ฐจ ์ ํ(onTimeslotClick)์์๋ ๋๊ฐ์ด ๊ทธ ์๊ฐ์ fresh ๊ฐ์ผ๋ก ๊ณ์ฐํด์ ๋ฃ์๋ค. ๊ทธ๋ฆฌ๊ณ useEffect๋ ์ญ์ ํ๋ค.
ํต์ฌ์ amounts์ buttonDisabled๊ฐ ํ ํธ๋์ญ์
์ ์ผ๊ด๋๊ฒ atom์ผ๋ก ๋ค์ด๊ฐ๋ค๋ ๊ฒ์ด๋ค. ๋ ๋ ์ฌ์ดํด์ ๊ธฐ๋ค๋ฆฌ์ง ์์ผ๋ ์ด๊ธ๋ ํ์ด ์๋ค.
๐ ๊ธฐ๋ ํจ๊ณผ / ์ฑ๋ฅ
- ํ ๋ฐ์ ๋ฆ๋ ์ฌ์ดํด ์ ๊ฑฐ → ํ์ด๋ฐ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์๋ ๊ตฌ์กฐ
flushSync๊ฐ์ ๊ฐ์ ๋๊ธฐ ๋ ๋ ๋ถํ์ → ์๋ ๋ฐฐ์นญ ์ ์ง- CTA๋ ๊ธฐ์กด์๋
floatingOptions๋ฅผ ๊ตฌ๋ ํ๊ณ ์์์ผ๋ฏ๋ก ์ถ๊ฐ ๋ ๋·์ ๊ท ๊ตฌ๋ ์๋ค. ๋์ผ write์ ํ๋ ํ๋๋ง ๋ ์ค๋ฆด ๋ฟ - ๋์ด๋ ๋น์ฉ์ ์ฌ์ฉ์ ์ธํฐ๋์ ๋น ์์ ํจ์ ํธ์ถ ๋ช ๋ฒ(๋ ๋ ๊ฒฝ๋ก ์๋)๋ฟ → ์ฌ์ค์ ์ํฅ ์์
๐ ํ๊ณ
์ด๋ฒ ์ด์๋ฅผ ํด๊ฒฐํ๋ฉด์ ๊ฐ์ฅ ํฌ๊ฒ ๋จ์ ๊ฑด ๋ ๊ฐ์ง๋ค.
์ฒซ์งธ, ํ์ ์ํ(derived state)๋ store์ ๋ณต์ ํ์ง ๋ง์. ๋ณต์ ํ๋ ์๊ฐ ์๋ณธ๊ณผ ๋ณต์ฌ๋ณธ ๋ ๊ฐ์ ์ง์ค์ด ์๊ธฐ๊ณ , ๋์ ๋ง์ถ๋ ๋๊ธฐํ ์ฑ ์์ด ๋ฐ๋ผ์จ๋ค. useEffect๋ก ๋ง์ถ๋ฉด ํ ๋ฐ์ ๋ฆ๊ณ , flushSync๋ก ๋ง์ถ๋ฉด ์ฑ๋ฅ์ ๊น๋๋ค. ๋ ๋ค ์ฆ์์ ๋ค๋ฃฐ ๋ฟ์ด๋ค.
๋์งธ, ๋ ๋ ์ค ๊ณ์ฐ๋ ๊ฐ์ "์ง์ ์ปค๋ฐ state" ๊ธฐ์ค์ด๋ค. ์ด๋ฒคํธ ํธ๋ค๋ฌ์์ ๋ฐฉ๊ธ setState๋ก ๋ฐ๊พผ ๊ฐ์ ๊ทธ ์๋ฆฌ์์ ์ฐ๊ณ ์ถ๋ค๋ฉด, state๋ฅผ ๋ค์ ์ฝ์ง ๋ง๊ณ ๋ด๊ฐ ๋ฐฉ๊ธ ๋ง๋ fresh ๊ฐ์ผ๋ก ์ง์ ๊ณ์ฐํด์ผ ํ๋ค. ๋๋ฌด ๋น์ฐํ ์ฌ์ค์ธ๋ฐ, "๊ฐ์ด ๋ฆ๋๋ค"๋ ์ฆ์์ ๋ง๋๋ฉด ์๊พธ ํ์ด๋ฐ API๋ถํฐ ์ฐพ๊ฒ ๋๋ค. ์ดํ ๋์ผํ ์ด์๊ฐ ์์ด๋ ๋นํฉํ๊ณ ํค๋งค์ง ๋ง์๋ ์ทจ์ง์์ ์ด ๊ธ์ ๋จ๊ธด๋ค.
๋๊ธ