๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
Frontend/React

[React] ์˜ˆ์•ฝ ์‹คํŒจ → PDP ์ƒˆ๋กœ๊ณ ์นจ, ๋ธŒ๋ผ์šฐ์ € ๊ฐ„ ๋ฉ”์‹œ์ง€ ํ†ต์‹ ์œผ๋กœ ํ•ด๊ฒฐํ•˜๊ธฐ

by YWTechIT 2025. 9. 28.
728x90

๐Ÿ“ ์˜ˆ์•ฝ ์‹คํŒจ โ†’ PDP ์ƒˆ๋กœ๊ณ ์นจ, ๋ธŒ๋ผ์šฐ์ € ๊ฐ„ ๋ฉ”์‹œ์ง€ ํ†ต์‹ ์œผ๋กœ ํ•ด๊ฒฐํ•˜๊ธฐ

๋ฌธ์ œ ์ƒํ™ฉ

๋†€์•ฑ์œผ๋กœ ์˜ˆ์•ฝ์ƒ์„ฑ ์‹คํŒจ ์•Œ๋Ÿฟ ํด๋ฆญ์‹œ ํ˜„์žฌ ํŽ˜์ด์ง€(์˜ˆ์•ฝ๊ฒฐ์ œํŽ˜์ด์ง€)๊ฐ€ ๋‹ซํžˆ๊ณ  PDPํŽ˜์ด์ง€๋กœ ๋Œ์•„์˜ค๊ฒŒ ๋œ๋‹ค. ์ด๋•Œ PDP๋Š” ์ƒํ’ˆ์˜ ์ตœ์‹  ์ •๋ณด(์ƒํ’ˆ/์˜ต์…˜/์•„์ดํ…œ ํŒ๋งค ๊ฐ€๋Šฅ, ๊ฐ€๊ฒฉ ๋“ฑ)๋ฅผ ๋‹ค์‹œ ๋ถˆ๋Ÿฌ์™€์•ผํ•œ๋‹ค. ๊ทธ๋ ‡์ง€์•Š์œผ๋ฉด ์œ ์ €๋Š” ์˜ˆ์•ฝ์ด ๋ถˆ๊ฐ€๋Šฅํ–ˆ๋˜ ๊ธฐ์กด ์Šค๋ƒ…์ƒท ์ •๋ณด๋ฅผ ๊ณ„์† ์‚ฌ์šฉํ•˜๊ฒŒ๋˜์–ด ์ข‹์ง€๋ชปํ•œ UX๋ฅผ ๊ฒฝํ—˜ํ•˜๊ฒŒ ๋œ๋‹ค.

๋Œ€์•ˆ ๋ถ„์„

  1. visibilityChange ์ด๋ฒคํŠธ ํ™œ์šฉ
  • ์žฅ์ : ๊ฐ„๋‹จํ•œ ๊ตฌํ˜„ (ํŽ˜์ด์ง€๊ฐ€ ๋‹ค์‹œ ํ™œ์„ฑํ™” ๋  ๋•Œ ์ƒํ’ˆ ์ •๋ณด API ํ˜ธ์ถœ)
  • ๋‹จ์ : ์˜ˆ์•ฝ ์‹คํŒจ ์ƒํ™ฉ๊ณผ ๋ฌด๊ด€ํ•˜๊ฒŒ ๋‹จ์ˆœํžˆ ํƒญ ์ „ํ™”๋งŒํ•ด๋„ API๋ฅผ ํ˜ธ์ถœํ•˜๊ฒŒ ๋œ๋‹ค. ์ด๋Š” ์‹ค์‹œ๊ฐ„์œผ๋กœ ์กฐํšŒํ•˜๋Š” API ํŠน์„ฑ์ƒ ๋ถˆํ•„์š”ํ•œ ๋„คํŠธ์›Œํฌ ๋น„์šฉ์ด ๋ฐœ์ƒํ•˜๊ฒŒ ๋˜์–ด ์„œ๋ฒ„/ํด๋ผ์ด์–ธํŠธ ๋ชจ๋‘ ๋ถ€๋‹ด์ด ํฌ๋‹ค.
useEffect(() => {
  const handleVisibliityChange = () => {
     // ์ƒํ’ˆ์ •๋ณด ์žฌํ˜ธ์ถœ
    queryClient.invalidateQueries({ queryKey: [FETCH_PRODUCT_KEY] })
    queryClient.invalidateQueries({ queryKey: [FETCH_ITEMS_KEY] })
  }
  addEventListener('visibilitychange', handleVisibliityChange)
  return () => {
    removeEventListener('visibilitychange', handleVisibliityChange)
  }
}, [id, queryClient])
  1. Observer ํŒจํ„ด์˜ eventBus + postMessage๋กœ ํŽ˜์ด์ง€ ํ†ต์‹ 
  • ์žฅ์ : ๋ฐœํ–‰/๊ตฌ๋… ๊ธฐ๋ฐ˜ ๊ตฌ์กฐ๋ผ ํŠน์ • ์ด๋ฒคํŠธ์—๋งŒ ๋ฐ˜์‘์ด ๊ฐ€๋Šฅํ•˜๋‹ค.
  • ๋‹จ์ : (1๋ฒˆ๋Œ€๋น„) ๋ณต์žกํ•œ ์ฝ”๋“œ ๋ฐ ๋ฆฌ์†Œ์Šค ์†Œ์š” (๋ฐœํ–‰/๊ตฌ๋… ๋กœ์ง ์ถ”๊ฐ€), ์ฝ”๋“œ ํŒŒํŽธํ™”(ํ•˜๋‚˜์˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด ๊ตฌ๋…/๋ฐœํ–‰ ์ฝ”๋“œ๋กœ์ธํ•ด ์—ฌ๋Ÿฌ ๊ณณ์— ํฉ์–ด์ ธ์žˆ์–ด ์ „์ฒด ํ”Œ๋กœ์šฐ๋ฅผ ํ•œ๋ฒˆ์— ์ดํ•ดํ•˜๋Š”๋ฐ ์–ด๋ ค์›€), ๊ฒฐ์ •์ ์œผ๋กœ ์ด ๋ฐฉ๋ฒ•์€ ํ˜„์žฌ ํด๋ผ์ด์–ธํŠธ์—์„œ ๋ฏธ์ง€์›ํ•˜๊ธฐ๋•Œ๋ฌธ์— ์‚ฌ์šฉ๋ถˆ๊ฐ€
// eventBus.ts
type EventCallback = (...args: any[]) => void;
export class EventBus {
  private events: Record<string, EventCallback[]> = {};
  subscribe(event: string, callback: EventCallback): () => void {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(callback);
    // Return unsubscribe function
    return () => {
      if (!this.events[event]) return;
      this.events[event] = this.events[event].filter(cb => cb !== callback);
      if (this.events[event].length === 0) {
        delete this.events[event];
      }
    };
  }
  publish(event: string, ...args: any[]): void {
    if (!this.events[event]) return;
    this.events[event].forEach(callback => {
      callback(...args);
    });
  }
}
export const EVENTS = {
  SERVICE_UPDATED: 'SERVICE_UPDATED',
  // Add more event constants as needed
};

// ์˜ˆ์•ฝ๊ฒฐ์ œ ํŽ˜์ด์ง€ (๋ฐœํ–‰)
const handleConfirm = () => {
  eventBus.publish(EVENTS.BOOKING_ERROR, { reason: "์˜ˆ์•ฝ ์ƒ์„ฑ ์‹คํŒจ" });

  window.postMessage(
    { type: EVENTS.BOOKING_ERROR, reason: "์˜ˆ์•ฝ ์ƒ์„ฑ ์‹คํŒจ" },
    window.location.origin
  );
};

// PDP (๊ตฌ๋…)
useEffect(() => {
  const unsubscribe = eventBus.subscribe(EVENTS.BOOKING_ERROR, (data) => {
    queryClient.invalidateQueries({ queryKey: [FETCH_PRODUCT_KEY] });
    queryClient.invalidateQueries({ queryKey: [FETCH_ITEMS_KEY] });
  });

  const handleMessage = (event: MessageEvent) => {
    if (event.origin !== window.location.origin) return;
    if (event.data?.type !== EVENTS.BOOKING_ERROR) return;

    queryClient.invalidateQueries({ queryKey: [FETCH_PRODUCT_KEY] });
    queryClient.invalidateQueries({ queryKey: [FETCH_ITEMS_KEY] });
  };

  window.addEventListener("message", handleMessage);

  return () => {
    unsubscribe();
    window.removeEventListener("message", handleMessage);
  };
}, [queryClient]);
  1. โœ… storage ์ด๋ฒคํŠธ
  • ์žฅ์ : ๋™์ผ ์ถœ์ฒ˜(sameOrigin)๋‚ด์—์„œ localStorage ๋ณ€๊ฒฝ์„ ์‹ค์‹œ๊ฐ„ ๊ฐ์ง€ ๊ฐ€๋Šฅ. ๊ฐ„๋‹จํ•œ ๋ฉ”์‹œ์ง€๋ฅผ ์ฃผ๊ณ  ๋ฐ›๊ธฐ์— ์ ์ ˆํ•˜๋‹ค.
  • ๋‹จ์ : ์ผํšŒ์„ฑ ํ†ต์‹ ์— ์ ํ•ฉํ•˜๋ฏ€๋กœ, ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ํ›„ ๋ฐ˜๋“œ์‹œ ํ•ด๋‹น key๋ฅผ ์ œ๊ฑฐํ•ด์•ผ ํ•œ๋‹ค.
// ์˜ˆ์•ฝ๊ฒฐ์ œํŽ˜์ด์ง€
const handleConfirm = () => {
  const destination = `/products/${productId}?startDate=${startDate}&endDate=${endDate}`

  if (yaNative?.isEnabled) {
    localStorage.setItem(BOOKING_ERROR_STORAGE_KEY, 'true')
    yaNative.dismiss()
  } else {
    // ๊ธฐ์กด ์ฒ˜๋ฆฌ
  }
}

// PDP
useEffect(() => {
  const handleStorageChange = (event: StorageEvent) => {
    const isBookingErrorEvent = event.key === BOOKING_ERROR_STORAGE_KEY

    if (!isBookingErrorEvent) {
      return
    }

    // ์ƒํ’ˆ์ •๋ณด ์žฌํ˜ธ์ถœ
    queryClient.invalidateQueries({ queryKey: [FETCH_PRODUCT_KEY] })
    queryClient.invalidateQueries({ queryKey: [FETCH_ITEMS_KEY] })

    localStorage.removeItem(BOOKING_ERROR_STORAGE_KEY)
  }

  window.addEventListener('storage', handleStorageChange)

  return () => {
    window.removeEventListener('storage', handleStorageChange)
  }
}, [queryClient])
  1. BroadCastChannel API
  • ์žฅ์ : ๊ตฌํ˜„์ด ๊ฐ„๋‹จํ•˜๊ณ , ์ด๋ฒคํŠธ ์ „์†ก ํ›„ ๋ณ„๋„์˜ cleanup(storage key ์ œ๊ฑฐ ๋“ฑ)์ด ํ•„์š” ์—†๋‹ค.
  • ๋‹จ์ : iOS safari ๋ธŒ๋ผ์šฐ์ € fallback ์ฒ˜๋ฆฌํ•„์š”(2022๋…„๋ถ€ํ„ฐ ์‚ฌ์šฉ์ด ๊ฐ€๋Šฅํ•˜๋‹ค.)
// ์˜ˆ์•ฝ๊ฒฐ์ œํŽ˜์ด์ง€
const handleConfirm = () => {
  const destination = `/products/${productId}?startDate=${startDate}&endDate=${endDate}`

  if (yaNative?.isEnabled) {
    const channel = new BroadcastChannel('booking_channel')
    channel.postMessage({ type: 'BOOKING_ERROR' })
    channel.close() // cleanup

    yaNative.dismiss()
  } else {
    // ๊ธฐ์กด ์ฒ˜๋ฆฌ
  }
}

// PDP
useEffect(() => {
  const channel = new BroadcastChannel('booking_channel')

  const handleMessage = (event: MessageEvent) => {
    if (event.data?.type !== 'BOOKING_ERROR') return

    // ์ƒํ’ˆ์ •๋ณด ์žฌํ˜ธ์ถœ
    queryClient.invalidateQueries({ queryKey: [FETCH_PRODUCT_KEY] })
    queryClient.invalidateQueries({ queryKey: [FETCH_ITEMS_KEY] })
  }

  channel.addEventListener('message', handleMessage)

  return () => {
    channel.removeEventListener('message', handleMessage)
    channel.close()
  }
}, [queryClient])

๊ฒฐ๋ก 

  • ์ด๋ฒˆ ์Šคํ”„๋ฆฐํŠธ์˜ ํ• ๋‹น๋ฐ›์€ ๋ฆฌ์†Œ์Šค, ํ˜ธํ™˜์„ฑ, ๋ชฉ์ ์„ฑ์„ ๊ณ ๋ คํ–ˆ์„๋•Œ storage ์ด๋ฒคํŠธ๋ฅผ ์‚ฌ์šฉํ•œ 3๋ฒˆ๋Œ€์•ˆ์ด ์ ์ ˆํ–ˆ๋‹ค. ๋ฌผ๋ก , BroadCastChannel์€ ๋” ์ง๊ด€์ ์ด๊ณ  ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ์‰ฌ์šด ๋ฐฉ์‹์ด์ง€๋งŒ, iOS Safari์˜ ํ˜ธํ™˜์„ฑ ์ด์Šˆ๊ฐ€ ์žˆ์–ด ๊ณง๋ฐ”๋กœ ๋„์ž…ํ•˜๊ธฐ๋Š” ์–ด๋ ค์› ๋‹ค.

ํ–ฅํ›„ ๊ฐœ์„  ๋ฐฉํ–ฅ์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

  1. BroadCastChannel์„ ์šฐ์„  ์ ์šฉํ•œ๋‹ค.
  2. BroadCastChannel์„ ์ง€์›ํ•˜์ง€ ์•Š๋Š” ๋ธŒ๋ผ์šฐ์ €์—์„œ๋Š” storage ์ด๋ฒคํŠธ๋กœ fallback ํ•œ๋‹ค.
  3. ์ถ”ํ›„ ๋ณ„๋„์˜ ์ด๋ฒคํŠธ ํ†ต์‹  ํŒจํ‚ค์ง€๋ฅผ ๋„์ž…ํ•œ๋‹ค๋ฉด EventBus๋กœ ์ผ์›ํ™” ํ•  ์ˆ˜ ์žˆ๋‹ค.
๋ฐ˜์‘ํ˜•

๋Œ“๊ธ€