πŸ’« ν† μŠ€ Frontend Fundamentals λͺ¨μ˜κ³ μ‚¬ 2회차 ν›„κΈ°

μ •μ†Œν˜„Β·2026λ…„ 3μ›” 24일

ν”„λ‘ νŠΈμ—”λ“œ 일지

λͺ©λ‘ 보기
10/10
post-thumbnail

ν† μŠ€ Frontend Fundamentals λͺ¨μ˜κ³ μ‚¬ 2회차 ν›„κΈ°

λ“€μ–΄κ°€λ©° ✍️

ν† μŠ€ Frontend Fundamentals λͺ¨μ˜κ³ μ‚¬ 2νšŒμ°¨μ— 1νšŒμ°¨μ— 이어 μ°Έμ—¬ν–ˆμ–΄μš”.

  • μ§„ν–‰ Β· μ˜€μ’…νƒλ‹˜
  • 1λΆ€ Β· ν•œμž¬μ—½λ‹˜
  • 2λΆ€ Β· λ¬Έλ™μš±λ‹˜

μ£Όμ–΄μ§„ νšŒμ˜μ‹€ μ˜ˆμ•½ μ‹œμŠ€ν…œ μ½”λ“œλ₯Ό λ¦¬νŒ©ν† λ§ν•˜λŠ” 게 이번 κ³Όμ œμ˜€μ–΄μš”. μ½”λ“œλ₯Ό 직접 κ°œμ„ ν•˜κ³ , μ„œλ‘œμ˜ PR을 λ¦¬λ·°ν•˜λ©΄μ„œ "쒋은 μ½”λ“œλž€ 무엇인가"에 λŒ€ν•΄ 깊이 κ³ λ―Όν•  수 μžˆλŠ” μ‹œκ°„μ΄μ—ˆμ–΄μš”. 이번 κΈ€μ—μ„œλŠ” μ„Έμ…˜μ—μ„œ 인상 κΉŠμ—ˆλ˜ λ‚΄μš©κ³Ό μ œκ°€ 직접 κ³ λ―Όν–ˆλ˜ 것듀을 ν•¨κ»˜ μ •λ¦¬ν•΄λ΄€μ–΄μš”.


우리의 μ½”λ“œλŠ” μ½λŠ” 것이 μ•„λ‹ˆλΌ μ˜ˆμΈ‘ν•œλ‹€

λͺ¨μ˜κ³ μ‚¬ λ¦¬λ·°μ—μ„œ κ°€μž₯ 기얡에 λ‚¨λŠ” λ§μ΄μ—ˆμ–΄μš”.

"λ‡ŒλŠ” λͺ¨λ“  것을 μž…λ ₯λ°›κ³  ν•΄μ„ν•˜λŠ” 게 μ•„λ‹ˆλΌ, μ–΄λŠ μ •λ„λŠ” μ˜ˆμΈ‘ν•œλ‹€."

μ½”λ“œλ₯Ό λ³Ό λ•Œ ν•œ 쀄 ν•œ 쀄 μ½κΈ°λ³΄λ‹€λŠ”, ν•¨μˆ˜λͺ…μ΄λ‚˜ ꡬ쑰λ₯Ό 보고 "이 λ‹€μŒμ—” μ΄λ ‡κ²Œ λ˜κ² μ§€" ν•˜κ³  μ˜ˆμΈ‘ν•˜λ©΄μ„œ μ½μž–μ•„μš”. κ·Έ 예츑이 λ§žμ•„λ–¨μ–΄μ§ˆμˆ˜λ‘ 읽기 νŽΈν•˜κ³ , λΉ—λ‚˜κ°€λ©΄ "μ–΄?" ν•˜κ³  λ©ˆμΆ”κ²Œ λ˜κ³ μš”.


κ·Έλž˜μ„œ μ˜ˆμΈ‘ν•˜κΈ° νž˜λ“  μ½”λ“œκ°€ 뭔데?

PR λ¦¬λ·°μ—μ„œλŠ” μ—¬λŸ¬ μ½”λ“œ 쀑 λͺ‡ κ°€μ§€ νŒ¨ν„΄μ„ λ½‘μ•„μ„œ 같이 μ΄μ•ΌκΈ°ν–ˆμ–΄μš”. "μ•„ 이런 게 예츑이 μ•ˆ λ˜λŠ” κ±°κ΅¬λ‚˜" μ‹Άμ—ˆλ˜ κ²ƒλ“€μ΄μ—μš”.

πŸ” setMessage β€” μ‚¬μš©μ²˜μ™€ λ©€μ–΄μ§ˆμˆ˜λ‘ μ΄λ¦„μ˜ 뢀담이 컀진닀

그쀑 ν•˜λ‚˜κ°€ message μƒνƒœμ˜€λŠ”λ°, ν₯λ―Έλ‘œμ› λ˜ 건 이름보닀도 전달 λ°©μ‹μ΄μ—ˆμ–΄μš”. λ©”μ‹œμ§€λ₯Ό μ‹€μ œλ‘œ μ‚¬μš©ν•˜λŠ” μ»΄ν¬λ„ŒνŠΈ κ°€κΉŒμ΄μ— 둬도 됐을 텐데, setMessageκ°€ props둜 계속 λ‚΄λ €λ°›λŠ” ν˜•νƒœμ˜€κ±°λ“ μš”.

const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);

const handleCancel = async (id: string) => {
  try {
    await cancelMutation.mutateAsync(id);
    setMessage({ type: 'success', text: 'μ˜ˆμ•½μ΄ μ·¨μ†Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.' });
  } catch {
    setMessage({ type: 'error', text: 'μ·¨μ†Œμ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.' });
  }
};

setMessageλΌλŠ” μ΄λ¦„λ§Œ λ΄μ„œλŠ” μ–΄λ–€ λ§₯락의 λ©”μ‹œμ§€μΈμ§€ β€” μ±„νŒ…μΈμ§€, μ•Œλ¦ΌμΈμ§€, ν”Όλ“œλ°±μΈμ§€ β€” μ„ μ–ΈλΆ€κΉŒμ§€ μ˜¬λΌκ°€μ„œ type: 'success' | 'error' ꡬ쑰λ₯Ό ν™•μΈν•΄μ•Όλ§Œ μ•Œ 수 μžˆμ–΄μš”. μ‚¬μš©μ²˜μ—μ„œ λ©€μ–΄μ§ˆμˆ˜λ‘ 이름이 μ Έμ•Ό ν•˜λŠ” 뢀담이 μ»€μ§€λŠ” κ±°μ˜ˆμš”.

데이터λ₯Ό μ“°λŠ” κ³³ κ°€κΉŒμ΄μ— λ‘λŠ” κ²ƒλ§ŒμœΌλ‘œλ„, 이름이 μ£ΌλŠ” 뢀담이 μžμ—°μŠ€λŸ½κ²Œ 쀄어듀 수 μžˆκ² λ‹€ μ‹Άμ—ˆμ–΄μš”.

πŸ” setDate β€” propsλŠ” λ‚΄λΆ€λ§Œ 바라봐야 ν•œλ‹€

또 λ‹€λ₯Έ νŒ¨ν„΄μ€ DatePicker에 date, setDateλ₯Ό 직접 λ„˜κΈ°λŠ” ν˜•νƒœμ˜€μ–΄μš”.

// ν˜ΈμΆœν•˜λŠ” μͺ½μ˜ λ§₯락이 κ·ΈλŒ€λ‘œ λ…ΈμΆœλœλ‹€
<div>
  <Text as="label">λ‚ μ§œ</Text>
  <input
    type="date"
    value={date}
    onChange={e => setDate(e.target.value)}
    min={formatDate(new Date())}
  />
</div>

μ„Έμ…˜μ—μ„œ 이런 이야기가 λ‚˜μ™”μ–΄μš”.

"ν•¨μˆ˜κ°€ μžμ‹ μ΄ ν˜ΈμΆœλ˜λŠ” μͺ½μ˜ λ§₯락을 κ°€μ§€λŠ” μˆœκ°„ μž¬μ‚¬μš©μ„±μ΄ λ–¨μ–΄μ§„λ‹€."

setDateλΌλŠ” 이름은 "λΆ€λͺ¨μ— dateλΌλŠ” stateκ°€ 있고 κ·Έκ±Έ setν•œλ‹€"λŠ” λ§₯락을 κ·ΈλŒ€λ‘œ λ“œλŸ¬λ‚΄μš”. DatePicker μž…μž₯μ—μ„œλŠ” "값이 λ°”λ€Œμ—ˆλ‹€"λŠ” κ²ƒλ§Œ μ•Œλ©΄ λ˜λŠ”λ°, ν˜ΈμΆœν•˜λŠ” μͺ½μ˜ μ‚¬μ •κΉŒμ§€ μ•Œκ³  μžˆλŠ” κ±°μ£ .

value와 onChangeλΌλŠ” 일반적인 μΈν„°νŽ˜μ΄μŠ€λ₯Ό λ”°λ₯΄λ©΄ 훨씬 μžμ—°μŠ€λŸ¬μ›Œμš”. label은 DatePicker의 관심사가 μ•„λ‹ˆλ‹ˆκΉŒ λ°”κΉ₯μ—μ„œ μ“°λŠ” μͺ½μ΄ μ±™κΈ°λ©΄ 되고, DatePickerλŠ” λ‚ μ§œ μž…λ ₯ κ·Έ μžμ²΄μ—λ§Œ μ§‘μ€‘ν•˜λ©΄ λΌμš”.

// βœ… DatePickerλŠ” λ‚ μ§œ μž…λ ₯만 μ±…μž„μ§„λ‹€
interface DatePickerProps {
  value?: string;
  defaultValue?: string;
  min?: string;
  onChange?: (e: ChangeEvent<HTMLInputElement>) => void;
  onBlur?: (e: FocusEvent<HTMLInputElement>) => void;
  name?: string;
}

export const DatePicker = forwardRef<HTMLInputElement, DatePickerProps>(
  ({ value, defaultValue, min, onChange, onBlur, name }, ref) => {
    const inputProps = value !== undefined ? { value } : { defaultValue };
    return (
      <input ref={ref} type="date" {...inputProps} min={min}
             onChange={onChange} onBlur={onBlur} css={inputStyle} />
    );
  }
);

onDateChange처럼 ꡳ이 μ€‘λ³΅λœ λ§₯락을 넣을 ν•„μš”λ„ μ—†μ–΄μš”. 이미 DatePickerλΌλŠ” μ»΄ν¬λ„ŒνŠΈ 이름이 λ§₯락을 μΆ©λΆ„νžˆ μ„€λͺ…ν•˜κ³  μžˆμœΌλ‹ˆκΉŒμš”.

πŸ” navigate('/', { state: { message } }) β€” μˆ¨κ²¨μ§„ 데이터 전달

이것도 같이 μ΄μ•ΌκΈ°ν–ˆλ˜ νŒ¨ν„΄μ΄μ—μš”.

navigate('/', { state: { message: 'μ˜ˆμ•½μ΄ μ™„λ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€!' } });

navigate의 state둜 λ©”μ‹œμ§€λ₯Ό λ„˜κΈ°λ©΄ "ν™ˆμœΌλ‘œ μ΄λ™ν•œλ‹€"κΉŒμ§€λŠ” μ˜ˆμΈ‘λ˜λŠ”λ°, "κ·ΈλŸ¬λ©΄μ„œ 성곡 λ©”μ‹œμ§€λ₯Ό μ „λ‹¬ν•œλ‹€"λŠ” 숨겨져 μžˆμ–΄μš”. λ°›λŠ” μͺ½μ—μ„œλŠ” location.stateλ₯Ό κΊΌλ‚΄μ„œ ν™•μΈν•˜κ³ , history.replaceState둜 μˆ˜λ™ μ •λ¦¬κΉŒμ§€ ν•΄μ•Ό ν•΄μš”.

// λ°›λŠ” μͺ½: μˆ¨κ²¨μ§„ 둜직이 ν•„μš”ν•˜λ‹€
const locationState = location.state as { message?: string } | null;
const [message, setMessage] = useState(
  locationState?.message ? { type: 'success', text: locationState.message } : null
);

useEffect(() => {
  if (locationState?.message) {
    window.history.replaceState({}, '');  // state μˆ˜λ™ μ •λ¦¬κΉŒμ§€ ν•„μš”
  }
}, [locationState]);

stateλŠ” λΈŒλΌμš°μ € νžˆμŠ€ν† λ¦¬μ— λ¬»ν˜€λ²„λ¦¬λŠ” 데이터라 디버깅도 μ–΄λ ΅κ³ , λ°›λŠ” μͺ½μ—λ„ μˆ¨κ²¨μ§„ 둜직이 ν•„μš”ν•΄μ Έμš”.

κ²°κ΅­ 이름이 역할을 μˆ¨κΈ°κ±°λ‚˜, 데이터가 보이지 μ•ŠλŠ” 경둜둜 μ „λ‹¬λ˜λ©΄ μ½λŠ” μ‚¬λžŒ μž…μž₯μ—μ„œ 예츑이 κΉ¨μ§„λ‹€λŠ” κ±°μ˜€μ–΄μš”.


PR λ¦¬λ·°ν•˜λ©΄μ„œ λ‚˜μ˜¨ 이야기듀

μ»΄ν¬λ„ŒνŠΈμ˜ λ°•μŠ€λͺ¨λΈ: 관심사λ₯Ό μ–΄λ””μ„œ λŠμ„ 것인가

좔상화λ₯Ό ν•  λ•Œ "μ–΄λ””κΉŒμ§€ 계측을 μ •ν•  거냐, μ–΄λ””κΉŒμ§€ μ˜λ„λ₯Ό λ‹΄μ•„μ„œ 보여쀄 거냐"κ°€ μ€‘μš”ν•˜λ‹€λŠ” 이야기가 λ‚˜μ™”μ–΄μš”. κ²°κ΅­ λ‚΄κ°€ μ„€κ³„ν•˜λŠ” μ»΄ν¬λ„ŒνŠΈκ°€ μ–΄λ””κΉŒμ§€λ₯Ό 자기 κ΄€μ‹¬μ‚¬λ‘œ λ³Ό 것인가λ₯Ό μ •ν•˜λŠ” κ±°λ”λΌκ³ μš”.

RoomBookingPage (쑰율자)
β”œβ”€β”€ BookingFilterSection (ν•„ν„° μž…λ ₯)
β”‚   β”œβ”€β”€ DatePicker
β”‚   β”œβ”€β”€ TimeSelect
β”‚   β”œβ”€β”€ AttendeesInput
β”‚   β”œβ”€β”€ FloorSelectField
β”‚   └── EquipmentSelector
β”œβ”€β”€ AvailableRoomList (κ²°κ³Ό ν‘œμ‹œ + 데이터 페칭)
└── SubmitButton (제좜)

각자 자기 λ°•μŠ€ μ•ˆμ˜ 일만 μ±…μž„μ§€λ©΄, λ‚˜μ€‘μ— ν•„ν„° UIκ°€ μ‚¬μ΄λ“œλ°”λ‘œ λ°”λ€Œμ–΄λ„, λͺ©λ‘μ΄ μΉ΄λ“œν˜•μœΌλ‘œ λ°”λ€Œμ–΄λ„ μ„œλ‘œ 영ν–₯을 μ•ˆ μ€˜μš”.

filterPanel: μˆ¨κΈ°λŠ” 게 λͺ©μ μ΄ 되면 μ•ˆ λœλ‹€

filterPanel도 μ½”λ“œ λ¦¬λ·°μ—μ„œ λ‚˜μ™”λ˜ μ΄μ•ΌκΈ°μ˜ˆμš”. μ—¬λŸ¬ 필터듀을 ν•˜λ‚˜μ˜ panel둜 묢은 κ΅¬μ‘°μ˜€λŠ”λ°, λ°”κΉ₯μ—μ„œ 보면 ν•„ν„° μ˜μ—­μ΄λΌλŠ” 건 μ•Œκ² λŠ”λ° μ•ˆμ— μ–΄λ–€ 정보가 μžˆλŠ”μ§€λŠ” κ°€λŠ μ΄ 잘 μ•ˆ λμ–΄μš”.

κ·ΈλŸ¬λ©΄μ„œ 였히렀 "ꡳ이 숨길 ν•„μš”κ°€ μžˆμ—ˆλ‚˜?"λΌλŠ” 생각이 λ“€μ—ˆμ–΄μš”. 좔상화 계측이 ν•˜λ‚˜ 더 생기면 λ°”κΉ₯μ—μ„œ 예츑이 μ‰¬μ›Œμ Έμ•Ό ν•˜λŠ”λ°, μ•ˆμ΄ μ•ˆ λ³΄μ΄λŠ” μ±„λ‘œ 묢이기만 ν–ˆλ‹€λ©΄ λΉ„μš© λŒ€λΉ„ μ–»λŠ” 게 μžˆλŠ”μ§€ 따져봐야 ν•˜λŠ” κ±°λ‹ˆκΉŒμš”.

μˆ¨κΈ°λŠ” 게 λͺ©μ μ΄ μ•„λ‹ˆλΌ, μ½λŠ” μ‚¬λžŒμ˜ μ˜ˆμΈ‘μ„ λ•λŠ” 게 λͺ©μ μ΄λΌλŠ” 게 이 λŒ€λͺ©μ—μ„œλ„ λ§žλ‹Ώμ•„ μžˆμ—ˆμ–΄μš”.

presentational vs λ‚΄λΆ€ 페칭 β€” 뭐가 λ§žμ•„μš”?

μ½”λ“œ 리뷰 μ‹œκ°„μ— λ‚˜μ˜¨ μ§ˆλ¬Έμ΄μ—μš”. 항상 presentational μ»΄ν¬λ„ŒνŠΈλ‘œ λ§Œλ“€μ–΄μ•Ό ν• μ§€, μ•„λ‹ˆλ©΄ λ‚΄λΆ€μ—μ„œ νŽ˜μΉ­ν•˜λ„λ‘ ν•΄μ•Ό ν• μ§€ κ³ λ―Όλœλ‹€λŠ” μ΄μ•ΌκΈ°μ˜€μ–΄μš”.

κ²°κ΅­ presentational이냐 λ‚΄λΆ€ νŽ˜μΉ­μ΄λƒλ³΄λ‹€, 이 μ»΄ν¬λ„ŒνŠΈμ˜ 핡심 관심사가 λ­”μ§€λ₯Ό λ¨Όμ € λ”°μ§€λŠ” 게 λ§žλŠ” 것 κ°™μ•„μš”. 도메인 λ§₯락을 μ₯κ³  μžˆμ–΄μ•Ό ν•˜λŠ” μ»΄ν¬λ„ŒνŠΈλΌλ©΄ λ‚΄λΆ€μ—μ„œ 직접 κ°€μ Έμ˜€λŠ” 게 더 μžμ—°μŠ€λŸ½κ³ , μˆœμˆ˜ν•˜κ²Œ UI만 μ±…μž„μ§€λŠ” μ»΄ν¬λ„ŒνŠΈλΌλ©΄ props둜 λ°›λŠ” 게 λ§žκ³ μš”. μ–΄λŠ μͺ½μ΄ 항상 μ˜³λ‹€κΈ°λ³΄λ‹€, μ»΄ν¬λ„ŒνŠΈκ°€ 무엇을 μ•Œμ•„μ•Ό ν•˜λŠ”μ§€μ— 따라 λ‹¬λΌμ§€λŠ” κ±°μ˜ˆμš”.

μ „μ—­ μƒνƒœμ—λŠ” 정말 전역인 κ²ƒλ§Œ λ‹΄μž

jotaiλ‚˜ zustand 같은 μ „μ—­ μƒνƒœ 라이브러리λ₯Ό μ“°λ©΄ νŽΈν•˜λ‹ˆκΉŒ 자꾸 여기에 μƒνƒœλ₯Ό λ„£κ³  싢은 유혹이 μƒκΈ°μž–μ•„μš”. 근데 ν•œλ²ˆ 물어봐야 ν•΄μš”. "이 μƒνƒœ, μ§„μ§œ λ‹€λ₯Έ νŽ˜μ΄μ§€μ—μ„œλ„ μ“°λ‚˜?"

λŒ€λΆ€λΆ„μ€ μ•„λ‹ˆμ—μš”. νšŒμ˜μ‹€ μ˜ˆμ•½ νŽ˜μ΄μ§€μ˜ ν•„ν„° 값은 κ·Έ νŽ˜μ΄μ§€μ—μ„œλ§Œ μ˜λ―Έκ°€ μžˆμ§€, ν™ˆ ν™”λ©΄μ΄λ‚˜ μ˜ˆμ•½ ν˜„ν™© νŽ˜μ΄μ§€μ—μ„œ μ•Œ ν•„μš”κ°€ μ—†κ±°λ“ μš”. 그런 μƒνƒœκΉŒμ§€ μ „μ—­ store에 λ„£μœΌλ©΄ "이 atom이 μ–΄λ””μ„œ μ“°μ΄λŠ” κ±°μ§€?" ν•˜κ³  좔적해야 ν•˜λŠ” λΉ„μš©μ΄ μƒκ²¨μš”.

μ „μ—­ μƒνƒœμ— λ‹΄κΈ° μ ν•©ν•œ 건 이런 κ²ƒλ“€μ΄μ—μš”.

  • 인증 정보 β€” μ–΄λ–€ νŽ˜μ΄μ§€μ—μ„œλ“  둜그인 μ—¬λΆ€λ₯Ό μ•Œμ•„μ•Ό ν•˜λ‹ˆκΉŒ
  • ν…Œλ§ˆ/μ–Έμ–΄ μ„€μ • β€” μ•± 전체에 영ν–₯을 μ£Όλ‹ˆκΉŒ
  • μ•Œλ¦Ό 카운트 β€” ν—€λ”μ—μ„œ 항상 λ³΄μ—¬μ€˜μ•Ό ν•˜λ‹ˆκΉŒ

λ°˜λŒ€λ‘œ νŠΉμ • νŽ˜μ΄μ§€μ˜ 폼 κ°’, ν•„ν„° 쑰건, λͺ¨λ‹¬ μ—΄λ¦Ό μƒνƒœ 같은 건 κ·Έ νŽ˜μ΄μ§€ μ•ˆμ—μ„œ ν•΄κ²°ν•˜λŠ” 게 λ§žμ•„μš”. URL searchParamsλ“ , μ§€μ—­ stateλ“ , μ‚¬μš©μ²˜μ—μ„œ κ°€κΉŒμš΄ 곳에 λ‘λŠ” κ±°μ£ .


정닡은 μ—†λ‹€, κ·Όκ±°κ°€ μžˆμ„ 뿐

λ§ˆμ§€λ§‰μœΌλ‘œ κ°€μž₯ μ™€λ‹Ώμ•˜λ˜ μ΄μ•ΌκΈ°μ˜ˆμš”.

"μ½”λ“œλ₯Ό μž‘μ„±ν•œ μ‚¬λžŒμœΌλ‘œμ„œ κ·Όκ±°λ₯Ό μ œμΆœν•  수 있으면 λœλ‹€."
"정닡이 μ•„λ‹ˆλ‹ˆκΉŒ. μ£Όμž₯이 μ€‘μš”ν•˜λ‹€. 정닡이라고 λ―Ώκ³  따라가면 μ•ˆ λœλ‹€."

κ°œλ°œν•˜λ‹€ 보면 "이게 λ§žλŠ” 건가?" 싢을 λ•Œκ°€ λ§Žμž–μ•„μš”. 근데 μ€‘μš”ν•œ 건 정닡을 μ°ΎλŠ” 게 μ•„λ‹ˆλΌ, λ‚΄κ°€ μ™œ μ΄λ ‡κ²Œ ν–ˆλŠ”μ§€ μ„€λͺ…ν•  수 μžˆλŠλƒλΌλŠ” κ±°μ˜ˆμš”.


λ‚΄ μ½”λ“œμ—μ„œ κ³ λ―Όν–ˆλ˜ 것듀

1. νŽ˜μ΄μ§€λŠ” μ•ˆλ‚΄ ν‘œμ§€νŒμ΄λ©΄ λœλ‹€

처음 RoomBookingPageλŠ” 402μ€„μ§œλ¦¬ 단일 νŒŒμΌμ΄μ—ˆμ–΄μš”. μƒνƒœ μ„ μ–Έ, URL 동기화, API 호좜, ν•„ν„° 검증, 필터링 둜직, λ Œλ”λ§μ΄ μ „λΆ€ ν•œ 곳에 μžˆμ—ˆκ±°λ“ μš”.

// ❌ Before: 원본 RoomBookingPage (402쀄)
export function RoomBookingPage() {
  const [date, setDate] = useState(searchParams.get('date') || formatDate(new Date()));
  const [startTime, setStartTime] = useState(searchParams.get('startTime') || '');
  // ... μƒνƒœ 8개

  useEffect(() => {
    // 6개 μƒνƒœλ₯Ό URLκ³Ό 일일이 동기화
    setSearchParams(params, { replace: true });
  }, [date, startTime, endTime, attendees, equipment, preferredFloor]);

  // 필터링 둜직, 좩돌 κ²€μ‚¬κΉŒμ§€ μ „λΆ€ 인라인으둜...

  return (
    <div css={css`background: ${colors.white}; padding-bottom: 40px;`}>
      {/* 300쀄 μ΄μƒμ˜ JSX */}
    </div>
  );
}

μ„Έμ…˜μ—μ„œ "κΈ°λŠ₯에 λŒ€ν•œ 정보가 μ•„λ‹ˆλΌλ©΄ μ•ˆλ‚΄ ν‘œμ§€νŒμ²˜λŸΌ μ™ΈλΆ€μ—μ„œ μ•ˆλ‚΄ν•˜κ³ , μ„ΈλΆ€ μ •λ³΄λŠ” μ•„λž˜μ— 남긴닀"λŠ” 이야기가 λ‚˜μ™”λŠ”λ°, λ”± νŽ˜μ΄μ§€ μ»΄ν¬λ„ŒνŠΈν•œν…Œ ν•΄λ‹Ήλ˜λŠ” 말이라고 λŠκΌˆμ–΄μš”.

λ¦¬νŒ©ν† λ§ν•˜λ©΄μ„œ νŽ˜μ΄μ§€ μ»΄ν¬λ„ŒνŠΈλ₯Ό νλ¦„λ§Œ λ³΄μ—¬μ£ΌλŠ” μ•ˆλ‚΄νŒμœΌλ‘œ λ§Œλ“€μ—ˆμ–΄μš”.

// βœ… After: μœ„μ—μ„œ μ•„λž˜λ‘œ μ½νžˆλŠ” 선언적 ꡬ쑰 
export function RoomBookingPage() {
  const navigate = useNavigate();
  const queryClient = useQueryClient();

  const {
    isFilterValid, errorMessage, isPendingBooking,
    selectedRoomId, handleRoomSelect, handleFilterChange, handleSubmit,
  } = useBookingForm();

  return (
    <div css={pageStyle}>
      <button onClick={() => navigate(ROUTES.HOME)}>← μ˜ˆμ•½ ν˜„ν™©μœΌλ‘œ</button>
      <Top.Top03>μ˜ˆμ•½ν•˜κΈ°</Top.Top03>

      {errorMessage && <Text color={colors.red500}>{errorMessage}</Text>}

      <BookingFilterSection onFilterChange={handleFilterChange} />

      {!isFilterValid ? (
        <Text>{'μ˜ˆμ•½ 쑰건을 λ¨Όμ € 선택해 μ£Όμ„Έμš”.'}</Text>
      ) : (
        <ErrorBoundary onReset={() => queryClient.resetQueries({ queryKey: ['availableRooms'] })}>
          <Suspense fallback={<AvailableRoomList.Loading />}>
            <AvailableRoomList selectedRoomId={selectedRoomId} onSelectRoom={handleRoomSelect} />
          </Suspense>
        </ErrorBoundary>
      )}

      <Button onClick={handleSubmit} disabled={!isFilterValid || isPendingBooking}>
        {isPendingBooking ? 'μ˜ˆμ•½ 쀑...' : 'ν™•μ •'}
      </Button>
    </div>
  );
}

파일 μ—΄μ—ˆμ„ λ•Œ "ν•„ν„° μ„ νƒν•˜κ³  β†’ λͺ©λ‘ 보고 β†’ ν™•μ •ν•˜λŠ” νŽ˜μ΄μ§€κ΅¬λ‚˜"κ°€ λ°”λ‘œ λ³΄μ—¬μš”. μ„ΈλΆ€ κ΅¬ν˜„μ΄ κΆκΈˆν•˜λ©΄ 타고 λ“€μ–΄κ°€λ©΄ λ˜κ³ μš”.

λ‹€λ§Œ μ§€κΈˆ μ½”λ“œλ₯Ό λ‹€μ‹œ 보면 μ•„μ‰¬μš΄ 뢀뢄도 μžˆμ–΄μš”. AvailableRoomList μ•ˆμ— "μ˜ˆμ•½ κ°€λŠ₯ νšŒμ˜μ‹€ N개" 같은 νƒ€μ΄ν‹€μ΄λ‚˜ 빈 μƒνƒœ λ©”μ‹œμ§€μ²˜λŸΌ 이 νŽ˜μ΄μ§€μ˜ λ§₯락을 λ‚˜νƒ€λ‚΄λŠ” λ‚΄μš©μ΄ μ»΄ν¬λ„ŒνŠΈ 내뢀에 λ“€μ–΄κ°€ μžˆκ±°λ“ μš”. λ‹€μ‹œ μž‘μ„±ν•œλ‹€λ©΄ 그런 뢀뢄은 νŽ˜μ΄μ§€μ—μ„œ 직접 λ‹΄λ‹Ήν•˜κ³ , μ»΄ν¬λ„ŒνŠΈλŠ” 정말 λͺ©λ‘ λ Œλ”λ§μ—λ§Œ μ§‘μ€‘ν•˜λ„λ‘ 더 μ’ν˜”μ„ 것 κ°™μ•„μš”.

2. URL searchParamsλ₯Ό μƒνƒœλ‘œ: μƒνƒœμ˜ μΆœμ²˜λŠ” ν•˜λ‚˜μ—¬μ•Ό ν•œλ‹€

이 뢀뢄은 μ½”λ“œλ₯Ό μ§œλ©΄μ„œ 저도 κ½€ κ³ λ―Όν–ˆλ˜ μ§€μ μ΄μ—μš”.

μ²˜μŒμ—” react-hook-form으둜 폼 μƒνƒœλ₯Ό κ΄€λ¦¬ν–ˆλŠ”λ°, 이 νŽ˜μ΄μ§€μ—μ„œλŠ” ν•„ν„° 쑰건이 URL에도 λ°˜μ˜λΌμ•Ό ν–ˆμ–΄μš”. λ’€λ‘œκ°€κΈ°λ„ λ˜μ–΄μ•Ό ν•˜κ³ , 링크둜 κ³΅μœ λ„ κ°€λŠ₯ν•΄μ•Ό ν•˜λ‹ˆκΉŒμš”.

λ¬Έμ œλŠ” react-hook-form이 κ΄€λ¦¬ν•˜λŠ” μƒνƒœμ™€ URL searchParamsλ₯Ό λ™κΈ°ν™”ν•˜λŠ” useEffectκ°€ λ”°λ‘œ ν•„μš”ν–ˆλ‹€λŠ” κ±°μ˜ˆμš”. μƒνƒœκ°€ 두 ꡰ데에 μžˆμœΌλ‹ˆκΉŒ 자꾸 싱크가 μ–΄κΈ‹λ‚˜λŠ” λŠλ‚Œμ΄μ—ˆμ–΄μš”.

// ❌ useState 8개 + useEffect둜 URL 동기화 = 이쀑 관리
const [date, setDate] = useState(searchParams.get('date') || formatDate(new Date()));
const [startTime, setStartTime] = useState(searchParams.get('startTime') || '');
// ... 6개 더

useEffect(() => {
  const params: Record<string, string> = {};
  if (date) params.date = date;
  if (startTime) params.startTime = startTime;
  // ...
  setSearchParams(params, { replace: true });
}, [date, startTime, endTime, attendees, equipment, preferredFloor, setSearchParams]);

κ·Έλž˜μ„œ μ•„μ˜ˆ URL searchParams 자체λ₯Ό μƒνƒœλ‘œ μ“°κΈ°λ‘œ ν–ˆμ–΄μš”.

// βœ… URL searchParamsκ°€ κ³§ μƒνƒœ (useBookingParams.ts)
export function useBookingParams() {
  const [searchParams] = useSearchParams();
  return {
    date: searchParams.get('date') ?? dayjs().format('YYYY-MM-DD'),
    start: searchParams.get('start') ?? '',
    end: searchParams.get('end') ?? '',
    attendees: Number(searchParams.get('attendees')) || 1,
    equipment: searchParams.get('equipment')?.split(',').filter(Boolean) ?? [],
    preferredFloor: searchParams.get('floor') ? Number(searchParams.get('floor')) : null,
  };
}
// βœ… κ°’ 변경도 ν•œ κ³³μ—μ„œ (useUpdateBookingParam.ts)
export function useUpdateBookingParam() {
  const [, setSearchParams] = useSearchParams();
  return (partial: Record<string, unknown>) => {
    setSearchParams(prev => {
      // partial의 값듀을 searchParams에 반영
    }, { replace: true });
  };
}

좜처λ₯Ό ν•˜λ‚˜λ‘œ μ’νžˆλ‹ˆκΉŒ 동기화 μ‹ κ²½ μ“Έ 게 μ—†μ–΄μ‘Œμ–΄μš”. useState 8개 + useEffect 동기화가 ν†΅μ§Έλ‘œ 사라진 μ…ˆμ΄μ—μš”.

μ΄λ ‡κ²Œ ν•˜λ©΄μ„œ 슀슀둜 λ‚΄λ¦° κ·Όκ±°λŠ” "URL이 single source of truthλ‹ˆκΉŒ 이쀑 관리λ₯Ό μ—†μ• μž"μ˜€μ–΄μš”. ν•„ν„° λ³€κ²½ β†’ URL λ³€κ²½ β†’ 쿼리 λ¦¬νŽ˜μΉ˜λΌλŠ” 단방ν–₯ 흐름이 더 예츑 κ°€λŠ₯ν•˜λ‹€κ³  λŠκΌˆκ³ μš”.

그런데 μ„Έμ…˜ 이후에 useBookingForm의 μΈν„°νŽ˜μ΄μŠ€μ— λŒ€ν•œ μΆ”κ°€ ν”Όλ“œλ°±μ„ λ°›μœΌλ©° 더 κ³ λ―Όν•΄λ³΄κ²Œ λμ–΄μš”.

const {
  isFilterValid, errorMessage, isPendingBooking,
  selectedRoomId, handleRoomSelect, handleFilterChange, handleSubmit,
} = useBookingForm();

form이 이름에 λ“€μ–΄κ°€λŠ” μˆœκ°„ μ„ΈλΆ€ κ΅¬ν˜„μ„ 보지 μ•Šμ•„λ„ react-hook-form의 useForm처럼 λ™μž‘ν•  κ±°λΌλŠ” κΈ°λŒ€λ₯Ό μ‹¬μ–΄μ£Όκ²Œ λœλ‹€λŠ” κ±°μ˜€μ–΄μš”. μ‹€μ œ μΈν„°νŽ˜μ΄μŠ€μ™€ μ–΄κΈ‹λ‚˜λ‹€ λ³΄λ‹ˆ μ˜ˆμΈ‘μ„ κΉ¨λŠ” 이름이 된 것 같기도 ν•˜κ³ μš”.

μƒκ°ν•΄λ³΄λ‹ˆ useBookingForm이 ν•„ν„° 쑰건(searchParams κ°’λ“€)κ³Ό μ˜ˆμ•½ μ•‘μ…˜(λ£Έ 선택, 제좜, μ—λŸ¬ 처리)을 ν•œκΊΌλ²ˆμ— μ₯κ³  μžˆλ‹€λŠ” 게 문제인 것 κ°™μ•„μš”. useBookingFilter와 useRoomBooking처럼 κ΄€μ‹¬μ‚¬λ³„λ‘œ λΆ„λ¦¬ν•œλ‹€λ©΄ 각 hook이 무엇을 λ°˜ν™˜ν•˜λŠ”μ§€ μ΄λ¦„λ§ŒμœΌλ‘œλ„ 더 λͺ…ν™•ν•˜κ²Œ μ˜ˆμΈ‘ν•  수 μžˆμ„ 것 κ°™μ•„μš”. 아직 ꡬ체적인 ν˜•νƒœλŠ” κ³ λ―Ό μ€‘μ΄μ§€λ§Œ, λΆ„λ¦¬ν•˜λŠ” λ°©ν–₯으둜 κ°œμ„ ν•΄λ³΄κ³  μ‹Άμ–΄μš”.


마무리 πŸ™Œ

이번 λͺ¨μ˜κ³ μ‚¬λ₯Ό ν•œ μ€„λ‘œ μš”μ•½ν•˜λ©΄, "μ™œ μ΄λ ‡κ²Œ μ§°μ–΄?"에 λŒ€λ‹΅ν•  수 μžˆλŠ” μ½”λ“œλ₯Ό μ“°μžμΈ 것 κ°™μ•„μš”.

예츑 κ°€λŠ₯ν•œ 이름, μƒνƒœμ˜ 좜처λ₯Ό ν•˜λ‚˜λ‘œ, λ°μ΄ν„°λŠ” μ“°λŠ” κ³³ κ°€κΉŒμ΄μ—, μ „μ—­ μƒνƒœλŠ” μ§„μ§œ 전역인 κ²ƒλ§Œ. λ‹€ κ²°κ΅­ "μ½λŠ” μ‚¬λžŒμ΄ μ˜ˆμΈ‘ν•  수 있게"λΌλŠ” ν•˜λ‚˜μ˜ μ›μΉ™μ—μ„œ λ‚˜μ˜€λŠ” μ΄μ•ΌκΈ°μ˜€μ–΄μš”.

μ•žμœΌλ‘œλ„ μ½”λ“œ μ§€ λ•Œ "이거 λ‹€λ₯Έ μ‚¬λžŒμ΄ 보면 예츑 κ°€λŠ₯ν•œκ°€?" ν•œλ²ˆ 더 μƒκ°ν•΄λ³΄κ²Œ 될 것 κ°™μ•„μš”.
이름 ν•˜λ‚˜, 데이터 μœ„μΉ˜ ν•˜λ‚˜κ°€ μ½λŠ” μ‚¬λžŒμ˜ 흐름을 λŠκΈ°λ„ ν•˜κ³  μžμ—°μŠ€λŸ½κ²Œ 이어주기도 ν•˜λ‹ˆκΉŒμš”.
κ·Έ 질문 ν•˜λ‚˜λ₯Ό μ•žμœΌλ‘œλ„ μŠ΅κ΄€μ²˜λŸΌ κ°€μ Έκ°€κ³  μ‹Άμ–΄μš”.

같은 μ½”λ“œλ₯Ό μž‘μ„±ν•˜κ³  μ„œλ‘œμ˜ 선택에 λŒ€ν•΄ "μ™œ?"λ₯Ό 묻고 λ‹΅ν•˜λŠ” μ‹œκ°„μ΄ λ°€λ„μžˆλŠ” 의미 μžˆλŠ” μ‹œκ°„μ΄μ—ˆμŠ΅λ‹ˆλ‹€..!

3νšŒμ°¨λ„ κΈ°λŒ€λ˜λ„€μš”!! πŸ™Œ

profile
κΈ°μˆ μ„ λ„˜μ–΄ μ œν’ˆμ˜ κ°€μΉ˜λ₯Ό λ§Œλ“œλŠ” ν”„λ‘ νŠΈμ—”λ“œ μ—”μ§€λ‹ˆμ–΄λ₯Ό μ§€ν–₯ν•©λ‹ˆλ‹€.

4개의 λŒ“κΈ€

comment-user-thumbnail
2026λ…„ 3μ›” 25일

잘 μ½μ—ˆμŠ΅λ‹ˆλ‹€~ 저도 μ†Œν˜„λ‹˜μ΄ 남겨주신 글을 보고 λ“  생각 λ‚¨κ²¨λ΄μš”!

navigate의 stateλ₯Ό μ΄μš©ν•΄μ„œ λ‹€λ₯Έ νŽ˜μ΄μ§€μ— 값을 λ„˜κ²¨μ£ΌλŠ” μ½”λ“œλŠ” message λΌλŠ” 이름이 맀우 일반적인 이름이기 λ•Œλ¬Έμ— μ˜λ„κ°€ λ“œλŸ¬λ‚˜μ§€ μ•ŠλŠ” 것 κ°™μ•„μ„œ 문제인 것 κ°™μ•„μš”. A -> B νŽ˜μ΄μ§€λ‘œ μ΄λ™ν•œλ‹€κ³  ν•  λ•Œ messageλ₯Ό B νŽ˜μ΄μ§€κ°€ μ™ΈλΆ€λ‘œ λ…ΈμΆœν•˜λŠ” 계약이라고 μƒκ°ν•œλ‹€λ©΄ B νŽ˜μ΄μ§€λ‘œ 이동할 λ•Œ λ°°λ„ˆλ₯Ό λ…ΈμΆœν•˜κ³  싢은 경우면 ν•΄λ‹Ή 값을 λ“€κ³  λΌμš°νŒ…ν•˜λŠ” 것이기 λ•Œλ¬Έμ— 크게 μ–΄μƒ‰ν•˜μ§€ μ•Šλ‹€κ³  μƒκ°ν•˜κ±°λ“ μš”. url queryλ₯Ό ν†΅ν•΄μ„œ νŠΉμ • νŽ˜μ΄μ§€μ— μ§„μž…ν–ˆμ„ λ•Œ λͺ¨λ‹¬μ„ λ„μš°λŠ” λ“±μ˜ νŒ¨ν„΄λ„ λΉ„μŠ·ν•˜λ‹€κ³  μƒκ°ν•˜κ΅¬μš”. 그리고 μž‘μ„±ν•΄μ£Όμ‹  λ°›λŠ” μͺ½μ˜ λ‘œμ§μ„ ν•˜λ‚˜μ˜ ν›…μœΌλ‘œ 좔상화λ₯Ό ν•΄λ³Ό 수 μžˆμ„ 것 κ°™μ•„μš”. const message = useOneTimeQueryParam('message'); 이 훅은 값을 μ½μ–΄μ™€μ„œ λ©”λͺ¨λ¦¬μ— μ €μž₯ν•˜κ³ , λ°”λ‘œ μ •λ¦¬ν•˜λŠ” λŠλ‚ŒμœΌλ‘œμš”. μ›λž˜ νŠΉμ • 객체(B νŽ˜μ΄μ§€)의 μž…μž₯μ—μ„œλŠ” "λ‹€λ₯Έ κ°μ²΄λ‘œλΆ€ν„° νŠΉμ • λ©”μ‹œμ§€λ₯Ό μ „λ‹¬λ°›μ•„μ„œ 그에 λ§žλŠ” 행동을 ν•˜κ² λ‹€" λΌλŠ” 약속을 ν•˜λŠ” 것이기에 μ—¬κΈ°μ„œλ„ B νŽ˜μ΄μ§€μ˜ μ±…μž„μ΄λ‚˜ 관심사λ₯Ό 잘 정해두면 무리가 μ—†λŠ” 것 κ°™μ•„μš”.

FilterPanel μ΄λΌλŠ” μ»΄ν¬λ„ŒνŠΈλŠ” μ•ˆμ— μ–΄λ–€ 정보가 μžˆλŠ”μ§€ μ•Œμ•„μ•Ό ν•˜κΈ° μ „μ—λŠ” 미리 μ•Œ ν•„μš”λŠ” μ—†λ‹€κ³  λŠλ‚„μˆ˜λ„ μžˆλ‹€κ³  μƒκ°ν•΄μš”. ν•„ν„° ν˜•νƒœμ˜ UIκ°€ 있고 화면에 λ³΄μ—¬μ§€λŠ” 데이터가 필터링을 κ±°μ³μ„œ 화면에 λ³΄μ—¬μ§ˆ 수 μžˆλ‹€λΌλŠ” λ§₯락을 μ•„λŠ” κ²ƒμœΌλ‘œ μΆ©λΆ„ν•  수 μžˆλ‹€κ³  λ³΄κ±°λ“ μš”. λ§Œμ•½ λͺ¨λ“  ν•„ν„°κ°€ 외뢀에 λ…ΈμΆœλ˜μ–΄ μžˆμ—ˆλ‹€λ©΄ μ €λŠ” "ꡳ이 λ…ΈμΆœν•  ν•„μš”κ°€ μžˆμ—ˆλ‚˜?"λΌλŠ” 생각을 μ—­μœΌλ‘œ ν•  수 μžˆμ„ 것 κ°™μ•„μš”.

λ¦¬νŒ©ν„°λ§μ„ ν•΄μ£Όμ‹  μ½”λ“œμ—μ„œ useBookingParams, useUpdateBookingParam은 ν•˜λ‚˜μ˜ ν›…μ—μ„œ κ΄€λ¦¬ν•˜λ©΄ μ’‹κ² λ‹€λŠ” 생각이 λ“€μ—ˆμ–΄μš”. url queryλΌλŠ” ν•˜λ‚˜μ˜ 데이터λ₯Ό 읽고 μ“°λŠ” μ±…μž„μ„ 같이 λ‹΄λ‹Ήν•  수 μžˆλ„λ‘μ΄μš”. λ Œλ”λ§ κ΄€μ μ—μ„œ μž₯점을 얻을 수 μžˆλ‹€κ³  λ³Ό 수 μžˆμ§€λ§Œ, search param νŠΉμ„±μƒ 큰 μž₯점을 λŠλΌκΈ°λŠ” μ–΄λ €μšΈ 것 κ°™μ•„μš”. 였히렀 2개의 훅을 μ‚¬μš©ν•΄μ•Ό ν•˜λ‹€λ³΄λ‹ˆ DX κ΄€μ μ—μ„œμ˜ 아쉬움이 μžˆμ„ 것 κ°™μ•„μš”.

useBookingForm은 κ°œμΈμ μœΌλ‘œλŠ” μ €λŠ” useForm의 μΈν„°νŽ˜μ΄μŠ€κ°€ κΈ°λŒ€λ˜μ§€λŠ” μ•Šκ³  μ˜ˆμ•½ form에 λŒ€ν•œ 데이터와 λ©”μ„œλ“œλ₯Ό 관리할 것이라고 κΈ°λŒ€ν•˜κ²Œ λ˜λŠ”λ°μš”. λ‹€λ§Œ, 문제라고 λŠκ»΄μ§€λŠ” 지점은 μ–˜κΈ°ν•΄μ£Όμ‹  것과 λΉ„μŠ·ν•˜κ²Œ form을 ν†΅ν•΄μ„œ μœ μ €μ—κ²Œ μž…λ ₯받은 값을 어디에 μ‚¬μš©ν•  κ²ƒμΈμ§€λŠ” λ‹€μ–‘ν•˜κ²Œ ν™•μž₯될 수 μžˆλŠ”λ°, ν˜„μž¬λŠ” κ·Έ μ‚¬μš©ν•˜λŠ” λ‘œμ§κΉŒμ§€ 같이 좔상화가 λ˜μ–΄ μžˆλ‹€λ³΄λ‹ˆ ν›…μ˜ 이름과 λ§žμ§€ μ•ŠλŠ” μ±…μž„μ„ κ°–κ³  μžˆλ‹€λŠ” 것과 이 폼에 μž…λ ₯된 값을 λ‹€λ₯Έ λͺ©μ μœΌλ‘œ 쓰기에 ν•΄λ‹Ή 훅을 μ‚¬μš©ν•˜λŠ” 것은 κ·Έ μ±…μž„μ΄ κ³Όν•˜κ² λ‹€λŠ” λŠλ‚Œμ΄ λ“œλŠ” 것 κ°™μ•„μš”.

1개의 λ‹΅κΈ€
comment-user-thumbnail
2026λ…„ 3μ›” 25일

AvailableRoomList λŠ” Suspenseλ₯Ό μ‚¬μš©ν•  λ•Œ ν”νžˆ λ³Ό 수 μžˆλŠ” μ–΄μ©” 수 μ—†λŠ” κ³„μΈ΅ν™”μΈλ°μš” μ €λŠ” 이걸 Suspensive 라이브러리둜 ν•΄κ²°ν•˜κ³€ ν•©λ‹ˆλ‹€γ…Žγ…Ž

https://suspensive.org/ko

1개의 λ‹΅κΈ€