[๐Ÿš— Log] Zustand ๊ฐ™์€ ๊ตฌ๋…ํ˜• ์Šคํ† ์–ด๋ฅผ ๋”ฐ๋ผํ•ด๋ณด๋ฉฐ ์—ฐ์Šตํ•˜๋Š” Observer pattern

์•ค๋”์†์”จยท2025๋…„ 7์›” 11์ผ
0
post-thumbnail

๐Ÿ‘€ ์ž‘์„ฑ์˜ ๊ณ„๊ธฐ

์ตœ๊ทผ ๊ฐ„๋‹จํ•œ ํ† ์ด ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋ฉด์„œ Context API์ฒ˜๋Ÿผ props๋ฅผ ์ด์šฉํ•˜์ง€ ์•Š๋Š” ์ƒํƒœ ์ „ํŒŒ ๋กœ์ง์ด ํ•„์š”ํ•ด์กŒ๋‹ค.
๊ธฐ์กด Context API๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์—ฌ์ „ํžˆ ๋ถˆํ•„์š”ํ•œ ๋ฆฌ๋ Œ๋”๋ง ์ด์Šˆ๋ฅผ ์™„์ „ํžˆ ํ”ผํ•˜๊ธฐ ์–ด๋ ค์› ๋‹ค.

๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ์˜์กดํ•˜๋ฉด ํŽธ๋ฆฌํ•˜์ง€๋งŒ, ์ž‘์€ ํ† ์ด ํ”„๋กœ์ ํŠธ์— zustand๋ฅผ ์„ค์น˜ํ•˜๋Š” ๊ฑด ๊ณผํ•˜๋‹ค๋Š” ์ƒ๊ฐ์ด ๋“ค์—ˆ๋‹ค.
๊ทธ๋Ÿฌ๋˜ ์ค‘, ํ•™์Šตํ•˜๋ฉด์„œ ์–ป์€ Observer ํŒจํ„ด ๊ธฐ๋ฐ˜์˜ ์ƒํƒœ ๊ด€๋ฆฌ ์•„์ด๋””์–ด๋ฅผ ๊ตฌํ˜„ํ•˜๋ฉด์„œ ์ฒดํ™”์‹œ์ผœ๋ณด์ž๋Š” ๊ฒฐ์‹ฌ์ด ๋“ค๊ฒŒ ๋˜์—ˆ๋‹ค.

์ฐธ๊ณ : Observer ํŒจํ„ด์— ๋Œ€ํ•œ ์ž์„ธํ•œ ๋‚ด์šฉ์€ Refactoring.Guru์˜ ๊ธ€ ๋ฅผ ์ถ”์ฒœํ•œ๋‹ค.

๐Ÿง Observer pattern?

์˜ˆ์ „์— ์ฝ์—ˆ๋˜ ๊ธ€์— '๋””์ž์ธ ํŒจํ„ด์„ ๊ณต๋ถ€ํ•˜๋Š” ๊ฒƒ์€ ์„ ๋Œ€ ์œ„๋Œ€ํ•œ ๊ฐœ๋ฐœ์ž๋“ค์˜ ๋ฉ‹์ง„ ๊ฐœ๋ฐœ ๋ฐฉ์‹์„ ์–ด๊นจ๋„˜์–ด์—์„œ ๋นŒ๋ ค๋‹ค ์“ธ ์ˆ˜ ์žˆ๋Š” ์ข‹์€ ๋ฐฉ๋ฒ•์ด๋‹ค.' ๋ผ๋Š” ๊ธ€์ด ๊ธฐ์–ต๋‚œ๋‹ค.

ํŒจํ„ด ์ถ”์ข…์ž๊นŒ์ง„ ์•„๋‹ˆ์–ด๋„, ์–ด๋А์ •๋„๋Š” ์ด๋Ÿฐ ๊ฐœ๋ฐœ ๋ฐฉ์‹์˜ ํŒจํ„ดํ™”๋จ์„ ๋”ฐ๋ผํ•˜๋Š” ๊ฒƒ์ด ๋งค์šฐ ์œ ์šฉํ•˜๋‹ค๋Š” ๊ฒƒ์„ ๊ทผ๋ž˜ ๋งŽ์ด ๋А๋ผ๊ณ  ์žˆ๋‹ค.

  1. ํŒจํ„ด ์ •์˜

    • ์˜ต์ €๋ฒ„ ํŒจํ„ด์€ ์ฃผ์ œ(Subject)์˜ ์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ, ๋“ฑ๋ก๋œ ์—ฌ๋Ÿฌ ๊ด€์ฐฐ์ž(Observer)์—๊ฒŒ ์ผ๋Œ€๋‹ค(one-to-many) ๋ฐฉ์‹์œผ๋กœ ์•Œ๋ฆผ์„ ์ „ํŒŒํ•˜๋Š” ๋””์ž์ธ ํŒจํ„ด์ด๋‹ค.
    • ํ”ํžˆ Pub/Sub(๋ฐœํ–‰/๊ตฌ๋…) ๋ชจ๋ธ๋กœ๋„ ๋ถˆ๋ฆฐ๋‹ค
  2. Subject ๊ตฌ์กฐ ์ •์˜

    • Instance State: ํŠน์ • ๊ด€์‹ฌ์‚ฌ๋ฅผ ๊ฐ–๋Š” ๊ตฌ์กฐ์˜ ์ƒํƒœ
    • Observer List: Observer์„ ์ €์žฅํ•˜๊ณ  ์žˆ๋Š” ์ƒํƒœ
    • Observer Register: ์ธ์ž๋กœ ๋ฐ›๋Š” ํ•จ์ˆ˜๋ฅผ Observer State์— ๋“ฑ๋ก ๋ฐ ํ•ด์ œ ํ•จ์ˆ˜ ๋ฆฌํ„ด
    • Change Notification: ์ธ์Šคํ„ด์Šค ์ƒํƒœ์˜ ๋ณ€ํ™”๊ฐ€ ์ผ์–ด๋‚  ์‹œ ์ด๋ฅผ ๋“ฑ๋ก๋œ Observer๋“ค์—๊ฒŒ ์ „ํŒŒํ•˜๋Š” ํ•จ์ˆ˜
  3. ๋™์ž‘ ํ๋ฆ„

    1. Subject์™€ 1:N ๊ด€๊ณ„๋กœ Observer๋“ค์ด ๋“ฑ๋ก(๊ตฌ๋…)
    2. Subject ์ƒํƒœ ๋ณ€๊ฒฝ ์‹œ Observer๋“ค์—๊ฒŒ Notification
    3. ๊ฐ Observer์˜ update() ๋ฉ”์„œ๋“œ ์‹คํ–‰ โ†’ ํ•„์š”ํ•œ ๋Œ€์‘ ์ˆ˜ํ–‰
    4. Observer๋Š” ์–ธ์ œ๋“  ๋“ฑ๋กยทํ•ด์ œ ๊ฐ€๋Šฅ
  4. ์žฅ๋‹จ์ 

    • ์žฅ์ 
      • Subject ๋ณ€๊ฒฝ์„ ์ž๋™ ๊ฐ์ง€ ํ›„ ๋ณ€๊ฒฝ์  ์ž๋™ ์‹คํ–‰.
      • OCP(๊ฐœ๋ฐฉ-ํ์‡„ ์›์น™) ์ค€์ˆ˜ โ†’ ๋ฐœํ–‰์ž ์ฝ”๋“œ ๋ณ€๊ฒฝ ์—†์ด ์˜ต์ €๋ฒ„ ์ถ”๊ฐ€ ๊ฐ€๋Šฅ
      • Single Responsibility Principle ์ค€์ˆ˜ โ†’ Subject๋Š” ๋“ฑ๋ก๋œ Observer ์‹คํ–‰์—๋งŒ ๊ด€์‹ฌ์ด ์žˆ์„ ๋ฟ, ๊ตฌ์ฒด์ ์ธ update ๋‚ด์šฉ์€ Observer์˜ ๊ด€์‹ฌ!
      • ๋А์Šจํ•œ ๊ฒฐํ•ฉ(loose coupling) ์œ ์ง€
    • ๋‹จ์ 
      • ์˜ต์ €๋ฒ„ ์•Œ๋ฆผ ์ˆœ์„œ ์ œ์–ด ์–ด๋ ค์›€ โ†’ ์ด๋Š” ๋“ฑ๋ก ๋‹น์‹œ์— ์šฐ์„ ์ˆœ์œ„ ๊ฒฐ์ • ๋กœ์ง์„ ์ถ”๊ฐ€ํ•˜๋ฉด ์–ด๋А์ •๋„ ๊ฐœ์„  ๊ฐ€๋Šฅ
      • ๊ตฌ๋… ํ•ด์ œ ๋ˆ„๋ฝ ์‹œ ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ์œ„ํ—˜

๐Ÿง ๊ทธ๋ž˜์„œ Observer๋ž‘ ๋ฆฌ์—‘ํŠธ ๊ตฌ๋…ํ˜• ์ƒํƒœ๊ด€๋ฆฌ๊ฐ€ ๋ฌด์Šจ์ƒ๊ด€์ธ๋ฐ?

๋ฆฌ์—‘ํŠธ์—์„œ๋Š” "Re-rendering" ์ด๋ผ๋Š” ํŠน์„ฑ์ƒ ํ•ญ์ƒ ์ƒํƒœ๊ด€๋ฆฌ์— ์žˆ์–ด ๋ฆฌ์—‘ํŠธ์˜ ํ™˜๊ฒฝ์— ์ข…์†๋  ์ˆ˜ ์—†๋‹ค๋Š” ์ ์„ ๋Š˜ ๊ณ ๋ คํ•  ์ˆ˜๋ฐ–์— ์—†๋‹ค.

๋Œ€ํ‘œ์ ์œผ๋กœ, ๋ฆฌ์•กํŠธ ์ƒํƒœ๊ฐ€ ์—…๋ฐ์ดํŠธ ๋˜์ง€ ์•Š์œผ๋ฉด ๋ฆฌ๋žœ๋”๋ง์ด ๋˜์ง€ ์•Š์•„ ์›นํŽ˜์ด์ง€์˜ ๋ณ€๋™์„ ๋ณผ ์ˆ˜ ์—†๋‹ค๋Š” ์ ์ด ๊ทธ ๋Œ€ํ‘œ์ ์ธ ์˜ˆ๋‹ค.

๊ทธ๋ ‡๋‹ค๋ฉด Zustand์™€ ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” ์–ด๋–ป๊ฒŒ ๋ชจ๋“ˆ ์Šค์ฝ”ํ”„ ๋‚ด์— ์žˆ๋Š” ์ธ๋ฉ”๋ชจ๋ฆฌ ์ƒํƒœ๊ฐ’์„ ์ด์šฉํ•ด์„œ ๋ฆฌ์•กํŠธ์™€ ์—ฐ๋™์„ ์‹œํ‚จ ๊ฒƒ์ผ๊นŒ?

๊ทธ๊ฒƒ์˜ ํ•ด๋‹ต์—๋Š” Observer์„ ์ด์šฉํ•œ ๋ฆฌ๋žœ๋”๋ง ์š”์ฒญ ์ „ํŒŒ ํ”„๋กœ์„ธ์Šค์— ๊ทธ ํ‚ค๊ฐ€ ์žˆ๋‹ค.

Zustand์˜ ๊ตฌ์กฐ์›๋ฆฌ๋ฅผ ๊ฐ„๋‹จํ•˜๊ฒŒ ๋”ฐ๋ผํ•ด๋ณธ ์˜ˆ์‹œ ์ฝ”๋“œ๋ฅผ ์˜ˆ๋กœ ๋“ค์–ด๋ณด๋ ค๊ณ  ํ•œ๋‹ค.

๐Ÿ’ก ์–ด๋””๊นŒ์ง€๋‚˜ ์ตœ๋Œ€ํ•œ ๊ฐ„๋žตํ•˜๊ฒŒ ๋งŒ๋“  ์˜ˆ์‹œ์ด๋‹ค. ์‹ค์ œ๋ก  ํ›จ์”ฌ ๋ณต์žกํ•˜๊ณ  ์•ˆ์ •์„ฑ์„ ์œ„ํ•œ ์ฝ”๋“œ๋„ ๋งŽ๋‹ค.

import { useReducer, useEffect, useRef, useMemo } from 'react';

/********************************************
 *
 * @createStore
 *
 ********************************************/

type Subscriber = () => void;
type Unsubscribe = () => void;
type PartialState<T> = Partial<T> | ((prev: T) => T);

export function createStore<T>(initialState: T) {
  let state = initialState;
  const subscribers = new Set<Subscriber>();

  return {
    // 1) ํ˜„์žฌ ์ƒํƒœ ์กฐํšŒ
    getState: () => state,

    // 2) ์ƒํƒœ ๋ณ€๊ฒฝ ๋ฐ ๊ตฌ๋…์ž ์•Œ๋ฆผ
    setState: (partial: PartialState<T>) => {
      state =
        typeof partial === 'function' ? (partial as (s: T) => T)(state) : { ...state, ...partial };

      for (const fn of subscribers) {
        fn?.();
      }
    },

    // 3) ๊ตฌ๋…ํ•˜๊ธฐ (์ปดํฌ๋„ŒํŠธ๊ฐ€ ์—ฌ๊ธฐ์— ๋“ฑ๋ก)
    subscribe: (fn: Subscriber): Unsubscribe => {
      subscribers.add(fn);
      return () => {
        subscribers.delete(fn);
      };
    },
  };
}

์œ„์˜ ๋‚ด์šฉ์€ Observer์„ ๊ด€๋ฆฌํ•˜๋Š” Subject ์ƒ์„ฑ ํ•จ์ˆ˜์ด๋‹ค.

์ด ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•  ๊ฒฝ์šฐ ํŠน์ • ๋ชจ๋“ˆ ์Šค์ฝ”ํ”„ ๋‚ด์—์„œ ์ดˆ๊ธฐํ™”๋˜์–ด ๋ฆฌ์•กํŠธ์˜ ๋ฆฌ๋žœ๋”๋ง ์‚ฌ์ดํด๊ณผ๋Š” ๋ฌด๊ด€ํ•œ ์˜์—ญ์—์„œ ์ „์—ญ์ ์œผ๋กœ ์œ ์ง€๊ฐ€ ๋œ๋‹ค.

export interface MainLayoutStoreState {
  headerMenuOpen: boolean;
}

// ํ•ด๋‹น ์Šคํ† ์–ด๋Š” ๋ชจ๋“ˆ ์Šค์ฝ”ํ”„ ๊ธฐ์ค€์œผ๋กœ ์ƒ๋ช…์ฃผ๊ธฐ๊ฐ€ ์œ ์ง€๋˜๋Š” ์ธ๋ฉ”๋ชจ๋ฆฌ ๋ฐ์ดํ„ฐ์ด๋‹ค.
export const mainLayoutStore = createStore<MainLayoutStoreState>({
  headerMenuOpen: false,
});

๊ทธ๋ ‡๋‹ค๋ฉด ์ด์ œ ์šฐ๋ฆฌ์—๊ฒŒ ํ•„์š”ํ•œ ๊ฒƒ์€ ์ด ๋‹จ์ˆœํ•œ ์ธ๋ฉ”๋ชจ๋ฆฌ ์ƒํƒœ์˜ ์Šคํ† ์–ด์˜ ์ƒํƒœ ์—…๋ฐ์ดํŠธ๋ฅผ react์˜ ๋žœ๋”๋ง ์‚ฌ์ดํด๊ณผ ์—ฐ๊ฒฐ์ง€์„ ์ˆ˜ ์žˆ๋Š” Store-Binding Hook์ด ํ•„์š”ํ•˜๊ฒŒ ๋œ๋‹ค.

๊ทธ๊ฒƒ์„ react hook๊ณผ react state๋ฅผ ์ด์šฉํ•ด์„œ ๊ตฌํ˜„์„ ํ•˜๋ฉด ๋œ๋‹ค.


/********************************************
 *
 * @useStore
 *
 ********************************************/

export function useStore<T>(store: ReturnType<typeof createStore<T>>): T;
export function useStore<T, U>(store: ReturnType<typeof createStore<T>>, sel: (state: T) => U): U;
export function useStore<T, U>(
  store: ReturnType<typeof createStore<T>>,
  sel?: (state: T) => U,
): T | U {
  const selector = useMemo(() => sel ?? ((state: T) => state as unknown as U), [sel]);
  const [, forceUpdate] = useReducer((x: number) => x + 1, 0);
  const lastSelected = useRef<U>(selector(store.getState()));

  // ํ›… ํ˜ธ์ถœ ํ›„ store ์—…๋ฐ์ดํŠธ์˜ ๊ฐ์ง€์— ๋ฐ˜์‘ํ•˜์—ฌ ๋ฆฌ๋žœ๋”๋งํ•˜๋Š” ์˜ต์ €๋ฒ„ ๋“ฑ๋ก
  useEffect(() => {
    const checkForUpdates = () => {
      const next = selector(store.getState());
      if (!Object.is(lastSelected.current, next)) {
        lastSelected.current = next;
        forceUpdate();
      }
    };

    const unsubscribe = store.subscribe(checkForUpdates);

    checkForUpdates();

    return unsubscribe;
  }, [store, selector]);

  return lastSelected.current;
}

์ด ํ›…์˜ ํ•ต์‹ฌ์€ ๋ฐ”๋กœ ์ž์‹ ์˜ ๋ฆฌ๋žœ๋”๋ง์„ ์œ ๋ฐœ์‹œํ‚ฌ ์ˆ˜ ์žˆ๋Š” Observer์„ props๋กœ ์ฃผ์ž…๋ฐ›๋Š” observer ๊ฐ์ฒด์˜ subscribe์œผ๋กœ ๋“ฑ๋ก์‹œํ‚จ๋‹ค๋Š” ์ ์— ์žˆ๋‹ค.

๋งŒ์•ฝ ์ด ํ›…์„ ์–ด๋–ค ํŠน์ • ์ปดํฌ๋„ŒํŠธ๊ฐ€ ํ˜ธ์ถœํ–ˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•˜์ž.

function WebMenu() {
  const { headerMenuOpen } = useStore(mainLayoutStore);
  ...

์ด ํ›…์ด ์ปดํฌ๋„ŒํŠธ ๋‚ด์—์„œ ํ˜ธ์ถœ๋˜์—ˆ๋‹ค๋Š” ์˜๋ฏธ๋Š” ๋ฐ”๋กœ

"๋‚ด๊ฐ€ ๋ฆฌ๋žœ๋”๋ง์— ์˜ํ–ฅ์„ ๋ฐ›์ง€ ์•Š์„ ref ์ƒํƒœ(lastSelected.current) ๋ฅผ ๋ณด์œ ํ•˜๊ณ  ์žˆ์„๊ฑด๋ฐ,

๋งŒ์•ฝ ์™ธ๋ถ€์— ์œ„์น˜ํ•œ store์˜ ์ƒํƒœ๊ฐ€ ์—…๋ฐ์ดํŠธ๊ฐ€ ๋˜๋ฉด ๋‚ด๊ฐ€ ๋“ฑ๋กํ•ด๋’€๋˜ observer์ธ checkForUpdates ๋ฅผ ํ˜ธ์ถœํ•ด์ฃผ์„ธ์š”.

์Šคํ† ์–ด ์ƒํƒœ์™€ ๋‚ด ref ์ƒํƒœ๋ฅผ ๋น„๊ตํ•ด๋ด์„œ ์ฐจ์ด๋‚˜๋ฉด ๋ฆฌ๋žœ๋”๋ง ํ•ด๋ฒ„๋ฆด๊ฑฐ์—์š”." ํ•˜๊ณ  ์„ ์–ธํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

Image

์ด๋กœ ์ธํ•ด์„œ ์™œ Zustand๊ฐ€ ์ž์‹ ์„ "๊ตฌ๋…ํ˜•" ์ƒํƒœ๊ด€๋ฆฌ๋ผ๊ณ  ์†Œ๊ฐœํ•˜๋Š”์ง€ ์ดํ•ดํ•  ์ˆ˜ ์žˆ๋‹ค.

์ปดํฌ๋„ŒํŠธ๋Š” ์ž์‹ ์˜ ๋ฆฌ๋žœ๋”๋ง์„ ๊ฒฐ์ •ํ•ด์ค„ ์ˆ˜ ์žˆ๋Š” ์˜ต์ €๋ฒ„ ๋“ฑ๋ก ํ›… "useStore" ๊ฐ€ ํ˜ธ์ถœ๋˜์ง€ ์•Š๋Š”๋‹ค๋ฉด ๋ถ€๋ชจ๊ฐ€ ๋ฆฌ๋žœ๋”๋งํ•˜์ง€ ์•Š๋Š” ์ด์ƒ
๋ณธ์ธ์€ ๋ฆฌ๋žœ๋”๋ง์„ ํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

// ๐Ÿšซ ์•„๋ž˜ ์ปดํฌ๋„ŒํŠธ๋Š” store ์—…๋ฐ์ดํŠธ์— ๋Œ€ํ•ด ๋ฐ˜์‘ํ•˜์ง€ ์•Š๋Š”๋‹ค.
// store์€ ์—…๋ฐ์ดํŠธ ๋˜๋”๋ผ๋„, ์ด๋ฅผ ๊ฐ์ง€ํ•  ์˜ต์ €๋ฒ„๋ฅผ ๋“ฑ๋กํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.
function WebMenu() {
  const { headerMenuOpen } = mainLayoutStore.getState();
  ...

// โœ… ์•„๋ž˜ ์ปดํฌ๋„ŒํŠธ๋Š” store ์—…๋ฐ์ดํŠธ์— ๋Œ€ํ•ด ๋ฐ˜์‘ํ•˜์—ฌ ๋ฆฌ๋žœ๋”๋งํ•œ๋‹ค.
// useStore์˜ ํ˜ธ์ถœ์ด ๋ฐ”๋กœ ์ž๊ธฐ ์ž์‹ ์˜ ๋ฆฌ๋žœ๋”๋ง ์œ ๋ฌด๋ฅผ ๊ฐ์ง€ํ•  ์˜ต์ €๋ฒ„๋ฅผ ๋“ฑ๋กํ•˜๋Š” ๋™์ž‘์ด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.
function WebMenu() {
  const { headerMenuOpen } = useStore(mainLayoutStore);
  ...

์ฆ‰, ๋‹ค์‹œ๋งํ•ด ์˜ต์ €๋ฒ„๋ฅผ ๋“ฑ๋กํ•˜๋Š” ํ–‰์œ„ === store ์ƒํƒœ๋ณ€ํ™”๋ฅผ "๊ตฌ๋…" ํ•˜๋Š” ํ–‰์œ„! ๊ฐ€ ๋˜๋Š” ๊ฒƒ์ด๋‹ค.

๐Ÿง ๋๋งบ์Œ

๋””์ž์ธ ํŒจํ„ด์€ ์œ„๋Œ€ํ•ด.

๊ทธ๋ฆฌ๊ณ , ๋ฐฐ์šฐ๊ณ  ์‹ค์ œ๋กœ ์ ์šฉํ•ด๋ณผ์ˆ˜๋ก ์ƒˆ์‚ผ์Šค๋Ÿฌ์šด ๊ฐœ๋…์ฒ˜๋Ÿผ ๋ณด์ด๋Š” ๊ฒƒ์ด ๋” ํฌ๊ฒŒ ๋‹ค๊ฐ€์˜ค๋Š” ๊ฒƒ ๊ฐ™๋‹ค.

๊ทธ๋ƒฅ ๋จธ๋ฆฟ์†์œผ๋กœ ์ดํ•ดํ•˜๊ณ  "์•„ ๊ทธ๋ž˜" ํ•˜๊ณ  ๋„˜์–ด๊ฐˆ ์ˆ˜ ์žˆ๋Š” ๋‚ด์šฉ๋„ ๋‹ค์‹œ ์—ฌ๋Ÿฌ๋ฒˆ ๋ณด๊ฒŒ ๋˜๋ฉด ์ƒˆ๋กœ์šด ๊นจ๋‹ฌ์Œ์ด ์˜ค๋Š” ์ˆœ๊ฐ„์ด ์žˆ๋‹ค.

๋”์šฑ ๊นŠ์€ ๊นจ๋‹ฌ์Œ์„ ์–ป์„ ์ˆ˜ ์žˆ๋„๋ก ๋…ธ๋ ฅํ•˜๋Š” ์ž์„ธ๋กœ ์žˆ์–ด์•ผ ํ•˜๊ฒ ๋‹ค.

profile
์ž๋ผ๋‚˜๋ผ ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ƒˆ์‹น!

0๊ฐœ์˜ ๋Œ“๊ธ€