Skip to content

[9νŒ€ 신홍쀀] Chapter 🦍 3-1. ν”„λ‘ νŠΈμ—”λ“œ ν…ŒμŠ€νŠΈ μ½”λ“œ 🦍#41

Open
jun17183 wants to merge 37 commits intohanghae-plus:hardfrom
jun17183:hard
Open

[9νŒ€ 신홍쀀] Chapter 🦍 3-1. ν”„λ‘ νŠΈμ—”λ“œ ν…ŒμŠ€νŠΈ μ½”λ“œ 🦍#41
jun17183 wants to merge 37 commits intohanghae-plus:hardfrom
jun17183:hard

Conversation

@jun17183
Copy link

@jun17183 jun17183 commented Aug 19, 2025

과제 μ…€ν”„νšŒκ³ 

기술적 μ„±μž₯

μ½”λ“œ ν’ˆμ§ˆ

ν•™μŠ΅ 효과 뢄석

과제 ν”Όλ“œλ°±

리뷰 λ°›κ³  싢은 λ‚΄μš©


HARD

7주차 과제 체크포인트

기본과제

  • 총 11개의 파일, 115개의 λ‹¨μœ„ ν…ŒμŠ€νŠΈλ₯Ό λ¬΄μ‚¬νžˆ μž‘μ„±ν•˜κ³  ν†΅κ³Όμ‹œν‚¨λ‹€.

질문

Q. handlersUtils에 남긴 μ§ˆλ¬Έμ— λ‹΅λ³€ν•΄μ£Όμ„Έμš”.

Q. ν…ŒμŠ€νŠΈλ₯Ό λ…λ¦½μ μœΌλ‘œ κ΅¬λ™μ‹œν‚€κΈ° μœ„ν•΄ μž‘μ„±ν–ˆλ˜ 섀정듀을 μ†Œκ°œν•΄μ£Όμ„Έμš”.

심화 과제

  • App μ»΄ν¬λ„ŒνŠΈ μ μ ˆν•œ λ‹¨μœ„μ˜ μ»΄ν¬λ„ŒνŠΈ, ν›…, μœ ν‹Έ ν•¨μˆ˜λ‘œ λΆ„λ¦¬ν–ˆλŠ”κ°€?
  • ν•΄λ‹Ή λͺ¨λ“ˆλ“€μ— λŒ€ν•œ μ μ ˆν•œ ν…ŒμŠ€νŠΈλ₯Ό 5개 이상 μž‘μ„±ν–ˆλŠ”κ°€?

과제 μ…€ν”„νšŒκ³ 

기술적 μ„±μž₯

κ·Έλ™μ•ˆ 과제λ₯Ό μ§„ν–‰ν•˜λ©° 처음으둜 ν…ŒμŠ€νŠΈ μ½”λ“œλ₯Ό λ§ˆμ£Όν–ˆμŠ΅λ‹ˆλ‹€. μ²˜μŒμ—λŠ” 과제 ν†΅κ³Όλ§Œμ„ 바라보닀 λ³΄λ‹ˆ ν…ŒμŠ€νŠΈ μ½”λ“œκ°€ λ―Έμ› μ§€λ§Œ, ν•­ν•΄ μ£Όμ°¨κ°€ 진행될 수둝 ν…ŒμŠ€νŠΈ μ½”λ“œμ— μ˜μ‘΄ν•˜λŠ” μžμ‹ μ„ λ°œκ²¬ν•  수 μžˆμ—ˆμŠ΅λ‹ˆλ‹€. 그러던 쀑 ν…ŒμŠ€νŠΈ μ½”λ“œκ°€ μ—†λŠ” 6μ£Όμ°¨ 과제λ₯Ό λ§ˆμ£Όν•˜μ˜€μŠ΅λ‹ˆλ‹€. μ„Έμ‚Ό ν…ŒμŠ€νŠΈ μ½”λ“œκ°€ κ·Έλ¦¬μ› μŠ΅λ‹ˆλ‹€. 이젠 ν…ŒμŠ€νŠΈ μ½”λ“œ 없이 μ‚΄ 수 μ—†λŠ” λͺΈμ΄ λ˜μ–΄λ²„λ ΈμŠ΅λ‹ˆλ‹€.

μ΄λ²ˆμ— 처음 μž‘μ„±ν•΄ λ³Έ ν…ŒμŠ€νŠΈ μ½”λ“œμ˜€μ§€λ§Œ, 되게 재밌고 잘 λ§žλ‹€λŠ” λŠλ‚Œμ΄ λ“€μ—ˆμŠ΅λ‹ˆλ‹€. 잘 λͺ¨λ₯΄λŠ” μƒνƒœμ—μ„œ λ– μ˜¬λ Έλ˜ ν…ŒμŠ€νŠΈ μ½”λ“œ 철학이 μ‹€μ œλ‘œ ν…ŒμŠ€νŠΈ μ½”λ“œμ—μ„œ μ€‘μš”ν•˜κ²Œ λ‹€λ£¨λŠ” κ°€μΉ˜μ˜€μŒμ— 기뻀고, ν…ŒμŠ€νŠΈ 흐름에 따라 순차적으둜 μž‘μ„±ν•˜λŠ” 것 λ˜ν•œ μ¦κ±°μ› μŠ΅λ‹ˆλ‹€. μ•„λ§ˆ μ•žμœΌλ‘œ ν…ŒμŠ€νŠΈ μ½”λ“œλ₯Ό 적극적으둜 μ‚¬μš©ν•΄ 보지 μ•Šμ„κΉŒ μƒκ°ν•©λ‹ˆλ‹€.



μ½”λ“œ ν’ˆμ§ˆ

1. 이벀트 νŒ©ν† λ¦¬

ν…ŒμŠ€νŠΈ μ½”λ“œλŠ” κ·Έ 자체둜 독립적이어야 쒋은 μ½”λ“œλΌκ³  μƒκ°ν–ˆμŠ΅λ‹ˆλ‹€. 이 생각은 λ©˜ν† λ§κ³Ό QnA μ‹œκ°„μ„ 톡해 λ”μš± κ²¬κ³ ν•΄μ‘ŒμŠ΅λ‹ˆλ‹€. κ·Έλž˜μ„œ μ΄ˆκΈ°μ—” λ‹€μŒκ³Ό 같이 직접 ν•˜λ“œμ½”λ”©ν•˜μ—¬ ν…ŒμŠ€νŠΈ 데이터λ₯Ό κ΄€λ¦¬ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

const events = [
  {
    title: '...',
    description: '...',
    ...
  }
];

μœ„μ™€ 같은 데이터λ₯Ό λ§€ μ½”λ“œλ§ˆλ‹€ μ„ μ–Έν•΄ 주어도 λ‚˜μ˜μ§€ μ•Šλ‹€κ³  μƒκ°ν•©λ‹ˆλ‹€. λ‹€λ§Œ 데이터 양이 λ§Žμ•„μ§€κ±°λ‚˜ 속성이 λ„ˆλ¬΄ 많으면 λ¬Έμ œκ°€ 될 수 μžˆλ‹€κ³  μƒκ°ν•©λ‹ˆλ‹€. ν…ŒμŠ€νŠΈ μ½”λ“œ 자체 만으둜 독립적인 것도 μ€‘μš”ν•œ κ°€μΉ˜μ§€λ§Œ, κ²°κ΅­ ν…ŒμŠ€νŠΈ μ½”λ“œλ„ μ‚¬λžŒμ΄ μ½λŠ” μ½”λ“œμ΄κΈ°μ— 가독성을 μœ„ν•΄μ„  μ–΄λŠ 정도 νƒ€ν˜‘μ΄ ν•„μš”ν•˜λ‹€κ³  μƒκ°ν–ˆμŠ΅λ‹ˆλ‹€.

μ²˜μŒμ—” fixture 폴더λ₯Ό λ§Œλ“€μ–΄ λͺ¨λ“  ν…ŒμŠ€νŠΈμ— μ μš©ν•  수 μžˆλŠ” 데이터λ₯Ό λ§Œλ“€μ—ˆμŠ΅λ‹ˆλ‹€. ν•˜μ§€λ§Œ ν•˜λ‚˜μ˜ 데이터 λͺ©λ‘μœΌλ‘œ ν…ŒμŠ€νŠΈλ₯Ό μ»€λ²„ν•˜κΈ°μ—” λ„ˆλ¬΄ λ§Žμ€ 데이터가 ν•„μš”ν•˜κ³ , 이 κ³Όμ •μ—μ„œ κ°€λ…μ„±μ΄λ‚˜ 데이터 μœ μ§€λ³΄μˆ˜ λ˜ν•œ νž˜λ“€μ–΄μ‘ŒμŠ΅λ‹ˆλ‹€.

λ‹€μŒμœΌλ‘œ μƒκ°ν•œ 방법은 νŒ©ν† λ¦¬μž…λ‹ˆλ‹€. μ²˜μŒμ— μΆ”κ΅¬ν•˜λ˜ 각 ν…ŒμŠ€νŠΈλ§Œμ˜ 독립성을 보μž₯ν•˜λ˜, κ³Όλ„ν•œ μ†μ„±μ΄λ‚˜ 데이터 양을 μ»€λ²„ν•˜κΈ°μ— νŒ©ν† λ¦¬κ°€ μ•ˆμ„±λ§žμΆ€μ΄λΌκ³  νŒλ‹¨ν–ˆμŠ΅λ‹ˆλ‹€.

import { Event, EventForm } from '../types';
import { generateEndTimeAfterStart, getRandomDate, getRandomTime } from './utils';

// 이벀트 폼
export const createEventForm = (override: Partial<EventForm> = {}): EventForm => {
  const startTime = getRandomTime();
  const endTime = generateEndTimeAfterStart(startTime);

  const defaults: EventForm = {
    title: `ν…ŒμŠ€νŠΈ 이벀트 ${crypto.randomUUID()}`,
    date: getRandomDate(),
    startTime,
    endTime,
    description: '',
    location: '',
    category: '',
    repeat: { type: 'none', interval: 0 },
    notificationTime: Math.floor(Math.random() * 60) + 1, // 1-60λΆ„
  };

  return { ...defaults, ...override };
};

// 이벀트 (id 포함)
export const createEvent = (override: Partial<Event> = {}): Event => {
  const eventFormDefaults = createEventForm();
  const defaults: Event = {
    id: override.id || crypto.randomUUID(),
    ...eventFormDefaults,
  };

  return { ...defaults, ...override };
};

// λͺ©λ‘
export const createEvents = (overrides: number | Partial<Event>[] = 0): Event[] => {
  if (typeof overrides === 'number') {
    return Array.from({ length: overrides }, () => {
      return createEvent();
    });
  }

  return overrides.map((override) => {
    return createEvent(override as Partial<Event>);
  });
};

μœ„ νŒ©ν† λ¦¬λ₯Ό 톡해 μ•„λž˜μ™€ 같이 μ½”λ“œλ₯Ό κ°„κ²°ν•˜κ²Œ μž‘μ„±ν•  수 μžˆμ—ˆμŠ΅λ‹ˆλ‹€.

it("검색어 '이벀트'와 μ£Όκ°„ λ·° 필터링을 λ™μ‹œμ— μ μš©ν•œλ‹€", () => {
    const events = createEvents([
      { date: '2025-07-01', title: '이벀트' },
      { date: '2025-07-01', title: '검색어 μ œμ™Έ' },
      { date: '2025-07-08', title: '이벀트' },
    ]);

    const expected: Event[] = [events[0]];

    const result = getFilteredEvents(events, '이벀트', new Date('2025-07-01'), 'week');

    expect(result).toEqual(expected);
    expect(result).toHaveLength(1);
});

handlers, handlersUtils

μ΄λ²€νŠΈλŠ” 생성, μˆ˜μ • 되면 fetchλ₯Ό λ‹€μ‹œ ν•΄ μƒνƒœλ₯Ό μ—…λ°μ΄νŠΈ ν•©λ‹ˆλ‹€. 이λ₯Ό μœ„ν•œ μ œμ–΄κ°€ ν•„μš”ν•  것 κ°™μ€λ°μš”.
μ–΄λ–»κ²Œ μž‘μ„±ν•΄μ•Ό ν…ŒμŠ€νŠΈκ°€ λ³‘λ ¬λ‘œ λŒμ•„λ„ μ•ˆμ •μ μ΄κ²Œ λ™μž‘ν• κΉŒμš”?
μ•„λž˜ 이름을 μ‚¬μš©ν•˜μ§€ μ•Šμ•„λ„ λ˜λ‹ˆ, λ…λ¦½μ μ΄κ²Œ ν…ŒμŠ€νŠΈλ₯Ό ꡬ동할 수 μžˆλŠ” 방법을 μ°Ύμ•„λ³΄μ„Έμš”. 그리고 이 λ‘œμ§μ„ PR에 μ„€λͺ…ν•΄μ£Όμ„Έμš”.

??
μ²˜μŒμ— 되게 μ˜μ•„ν–ˆλ˜ handlersUtils의 μ£Όμ„μž…λ‹ˆλ‹€. κ·Έλƒ₯ λŠλ‹·μ—†μ΄ handlers와 handlersUtilsκ°€ μžˆμœΌλ‹ˆ 이게 어디에 μ“°λŠ” 건지도 감이 μž‘νžˆμ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. μš°μ„  mock 폴더 μ•ˆμ— μžˆμœΌλ‹ˆ μ‹€μ œ μ„œλ²„ 톡신 λŒ€μ‹ μ„ μœ„ν•œ μž₯치인 건 같은데,,, μ™œ 2개둜 λΆ„λ¦¬λ˜μ–΄ μžˆμ§€...?

제 생각에 handlersλŠ” μ—¬λŸ¬ ν…ŒμŠ€νŠΈλ₯Ό μ „μ²΄μ μœΌλ‘œ 흐름에 따라 μ§„ν–‰ν•˜λ©° ν•˜λ‚˜μ˜ 데이터λ₯Ό 바라보기 μœ„ν•¨μ΄κ³ , handlerUtilsλŠ” λ³‘λ ¬μ μœΌλ‘œ 데이터λ₯Ό κ΄€λ¦¬ν•˜λ©° ν…ŒμŠ€νŠΈλ₯Ό μ§„ν–‰ν•˜κΈ° μœ„ν•¨μ΄λΌκ³  νŒλ‹¨ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

κ·Έλ ‡κΈ° λ•Œλ¬Έμ— μ²˜μŒμ—” handlersλ₯Ό λ‹€μŒκ³Ό 같이 μž‘μ„±ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

const data = { events: [ ...initialEvents ] }  // μ „μ—­

export handlers = [
  http.get( ... ),
  http.post( ... ),
]

그러고 handlersUtilsλŠ” ν΄λ‘œμ €λ₯Ό ν™œμš©ν•΄ μž‘μ„±ν•˜λ € ν–ˆμ§€λ§Œ handlers에 μžˆλŠ” μ„œλ²„ μš”μ²­ μ½”λ“œ 뢀뢄이 μ€‘λ³΅μœΌλ‘œ ν•„μš”ν–ˆμŠ΅λ‹ˆλ‹€. 이 뢀뢄이 λ§ˆμŒμ— λ“€μ§€ μ•Šμ•„ λ‹€μŒκ³Ό 같이 μˆ˜μ •ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

export const createHandler = (initialEvents: Event[] = []) => {
  const data = {
    events: initialEvents.length > 0 ? initialEvents : [...events],
  };

  return [
    http.get('/api/events', () => {
      return HttpResponse.json({ events: data.events });
    }),

    http.post('/api/events', async ({ request }) => {
      const eventData = (await request.json()) as EventForm;
      const newEvent = { id: crypto.randomUUID(), ...eventData };
      data.events.push(newEvent);

      return HttpResponse.json({ events: data.events }, { status: 201 });
    }),

    http.put('/api/events/:id', async ({ params, request }) => {
      const { id } = params;
      const eventIndex = data.events.findIndex((event) => event.id === id);
      if (eventIndex > -1) {
        const eventData = (await request.json()) as EventForm;
        data.events[eventIndex] = { ...data.events[eventIndex], ...eventData };
        return HttpResponse.json({ events: data.events[eventIndex] });
      }
      return HttpResponse.json({ message: 'Event not found' }, { status: 404 });
    }),

    http.delete('/api/events/:id', ({ params }) => {
      const { id } = params;
      data.events = data.events.filter((event) => event.id !== id);
      return HttpResponse.json({ events: null }, { status: 204 });
    }),
  ];
};

export const handlers: HttpHandler[] = createHandler();

덕뢄에 handlersUtilsλŠ” κ°„λ‹¨ν•˜κ²Œ μž‘μ„±ν•  수 μžˆμ—ˆμŠ΅λ‹ˆλ‹€.

export const setupMockHandler = (initEvents: Event[] = []) => {
  const events = [...initEvents];
  const handlers = createHandler(events);
  server.use(...handlers);
};

μΆ”κ°€λ‘œ μ•„λž˜μ™€ 같은 μ„œλ²„ μ—λŸ¬λ₯Ό μ „μ œλ‘œ ν•œ ν…ŒμŠ€νŠΈλ₯Ό μœ„ν•œ handlers도 μž‘μ„±ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

describe('useEventOperations 500 Error', () => {
  beforeEach(() => {
    setupMockErrorHandler();
  });

  it("이벀트 λ‘œλ”© μ‹€νŒ¨ μ‹œ '이벀트 λ‘œλ”© μ‹€νŒ¨'λΌλŠ” ν…μŠ€νŠΈμ™€ ν•¨κ»˜ μ—λŸ¬ ν† μŠ€νŠΈκ°€ ν‘œμ‹œλ˜μ–΄μ•Ό ν•œλ‹€", async () => {
    const { result } = renderHook(() => useEventOperations(true));

    await act(async () => {
      await result.current.fetchEvents();
    });

    expect(enqueueSnackbarFn).toHaveBeenCalledWith('이벀트 λ‘œλ”© μ‹€νŒ¨', { variant: 'error' });
  });

  it("λ„€νŠΈμ›Œν¬ 였λ₯˜ μ‹œ '일정 μ‚­μ œ μ‹€νŒ¨'λΌλŠ” ν…μŠ€νŠΈκ°€ λ…ΈμΆœλ˜λ©° 이벀트 μ‚­μ œκ°€ μ‹€νŒ¨ν•΄μ•Ό ν•œλ‹€", async () => {
    const { result } = renderHook(() => useEventOperations(true));

    await act(async () => {
      await result.current.deleteEvent('notExistEvent');
    });

    expect(enqueueSnackbarFn).toHaveBeenCalledWith('일정 μ‚­μ œ μ‹€νŒ¨', { variant: 'error' });
  });
});
export const createErrorHandler = () => {
  return [
    http.get('/api/events', () => {
      return HttpResponse.json({ message: 'Error' }, { status: 500 });
    }),

    http.post('/api/events', () => {
      return HttpResponse.json({ message: 'Error' }, { status: 500 });
    }),

    http.put('/api/events/:id', () => {
      return HttpResponse.json({ message: 'Error' }, { status: 500 });
    }),

    http.delete('/api/events/:id', () => {
      return HttpResponse.json({ message: 'Error' }, { status: 500 });
    }),
  ];
};

3. 순차적인 ν…ŒμŠ€νŠΈ

ν…ŒμŠ€νŠΈ μ½”λ“œλ₯Ό μž‘μ„±ν•˜λ©° λŠλ‚€ 점 쀑 ν•˜λ‚˜λŠ” 이 μ½”λ“œκ°€ μ •ν™•ν•˜κ²Œ ν…ŒμŠ€νŠΈν•˜κ³  μžˆμ§€ μ•Šμ„ μˆ˜λ„ μžˆκ² κ΅¬λ‚˜ 싢은 μ μž…λ‹ˆλ‹€. κ·Έμ € λ²„νŠΌμ„ λˆ„λ₯΄κ³  λͺ©λ‘μ˜ lengthκ°€ 1 더 λŠ˜μ—ˆμŒμ„ ν™•μΈν•˜λ©΄ λ˜λŠ” 것이 μ•„λ‹ˆλΌ, λ Œλ”λ§μ΄ λ˜μ—ˆμŒμ„ ν™•μΈν•˜κ³ , λ²„νŠΌμ„ λˆ„λ₯Ό μƒνƒœμž„μ„ ν™•μΈν•˜κ³ , 기쑴의 데이터와 λΉ„κ΅ν•˜μ—¬ μƒˆλ‘œμš΄ 데이터가 μΆ”κ°€λ˜μ—ˆμŒμ„ ν™•μΈν•˜μ—¬μ•Ό ν–ˆμŠ΅λ‹ˆλ‹€. κ·Έλž˜μ„œ λ‹€μŒκ³Ό 같이 κΌΌκΌΌν•˜κ²Œ ν…ŒμŠ€νŠΈλ₯Ό μž‘μ„±ν•˜λ„λ‘ λ…Έλ ₯ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

it('κΈ°μ‘΄ μΌμ •μ˜ μ„ΈλΆ€ 정보λ₯Ό μˆ˜μ •ν•˜κ³  변경사항이 μ •ν™•νžˆ λ°˜μ˜λœλ‹€', async () => {
    const events = createEvents([{ title: '기쑴 회의', date: todayDate }]);

    setupMockHandler(events);

    render(<RenderApp />);

    // App λ Œλ”λ§ ν…ŒμŠ€νŠΈ
    await expect(screen.getByText('일정 보기')).toBeInTheDocument();

    // μˆ˜μ • μ „ 일정 1건 확인
    expect(await screen.findAllByTestId('event-item')).toHaveLength(1);

    const editButtons = screen.getAllByRole('button', { name: 'Edit event' });
    expect(editButtons).toHaveLength(1);

    const user = userEvent.setup();

    // 일정 μˆ˜μ • λ²„νŠΌ 클릭
    await user.click(editButtons[0]);

    // μˆ˜μ • 폼 λ³€κ²½ 확인
    expect(screen.getByRole('button', { name: '일정 μˆ˜μ •' })).toBeInTheDocument();
    expect(screen.getByLabelText('제λͺ©')).toHaveValue('κΈ°μ‘΄ 회의');

    // 일정 μˆ˜μ • 폼 μž…λ ₯
    await inputEvent(user, { title: 'μˆ˜μ •λœ 회의' });

    // 일정 μˆ˜μ • λ²„νŠΌ 클릭
    await user.click(screen.getByRole('button', { name: '일정 μˆ˜μ •' }));

    // μˆ˜μ • ν›„ 일정 λͺ©λ‘ 확인
    const listItems = await screen.findAllByTestId('event-item');
    expect(listItems).toHaveLength(1);
    expect(listItems[0]).toHaveTextContent('μˆ˜μ •λœ 회의');
  });


ν•™μŠ΅ 효과 뢄석

μš°μ„  처음으둜 ν…ŒμŠ€νŠΈ μ½”λ“œλ₯Ό μž‘μ„±ν•΄ λ³΄μ•˜λ‹€λŠ” 점이 κ°€μž₯ κ³ λ¬΄μ μž…λ‹ˆλ‹€. 마λƒ₯ ν…ŒμŠ€νŠΈ μ½”λ“œλŠ” ν•„μš”ν•˜λ‹€, ν•˜μ§€λ§Œ ν•œ λ²ˆλ„ 해보지 μ•Šμ•˜λ‹€λŠ” μƒκ°λ§Œ μžˆμ—ˆλŠ”λ° μ΄μ œλŠ” ν…ŒμŠ€νŠΈ μ½”λ“œλ₯Ό μž‘μ„±ν•  수 μžˆλ‹€κ³  말할 수 μžˆμ„ κ²ƒλ§Œ κ°™μŠ΅λ‹ˆλ‹€.

λ¬Όλ‘  ν…ŒμŠ€νŠΈ μ½”λ“œ λΌμ΄λΈŒλŸ¬λ¦¬μ™€ 같은 μ‚¬μš©λ²•μ€ λ‹Ήμ—°ν•˜κ³  μ–΄λ–€ ν…ŒμŠ€νŠΈ μ½”λ“œκ°€ 쒋은 ν…ŒμŠ€νŠΈ μ½”λ“œμΈμ§€ λ”μš± κ³΅λΆ€ν•˜κ³  λˆˆμ— 많이 μ΅ν˜€λ‘˜ ν•„μš”κ°€ μžˆλ‹€κ³  λŠκΌˆμŠ΅λ‹ˆλ‹€. ν…ŒμŠ€νŠΈ μ½”λ“œλŠ” 기쑴에 μž‘μ„±ν•˜λ˜ μ½”λ“œλ“€λ³΄λ‹€ ν•œ 쀄 ν•œ μ€„μ˜ μ˜λ―Έκ°€ λ”μš± μ§™λ‹€κ³  λŠλ‚λ‹ˆλ‹€. λ°˜λŒ€λ‘œ λ”μš± 정닡이 없기도 ν•œ 것 κ°™μŠ΅λ‹ˆλ‹€. 그렇기에 λ”μš± 많이 κ²½ν—˜ν•˜κ³  μ‹œμ•Όλ₯Ό λ„“νž ν•„μš”κ°€ μžˆλ‹€κ³  μƒκ°ν•©λ‹ˆλ‹€.

싀무 적용 κ°€λŠ₯성은 맀우 λ†’λ‹€κ³  μƒκ°ν•©λ‹ˆλ‹€. λ‹€λ§Œ ν”„λ‘ νŠΈμ—”νŠΈ ν…ŒμŠ€νŠΈ μ½”λ“œ νŠΉμ„±μƒ 미리 μž‘μ„±ν•΄ λ‘κΈ°λŠ” μ–΄λ ΅λ‹€κ³  μƒκ°ν•©λ‹ˆλ‹€. μ•„λ¬΄λž˜λ„ μ΅œμ†Œν•œμ˜ μΈν„°λ ‰μ…˜μ€ κ°€λŠ₯ν•œ μˆ˜μ€€κΉŒμ§„ 개발이 ν•„μš”ν•˜κΈ°λ„ ν•˜λ©° μ‹€μ œ 화면이 μ–΄λ–»κ²Œ λ‚˜μ˜¬μ§€ μ˜ˆμΈ‘ν•˜κΈ΄ 쉽지 μ•ŠκΈ° λ•Œλ¬Έμž…λ‹ˆλ‹€. κ·ΈλŸΌμ—λ„ ν…ŒμŠ€νŠΈ μ½”λ“œλŠ” 맀우 ν•„μš”ν•œ 쑴재라고 μƒκ°ν•©λ‹ˆλ‹€. μ‹€λ¬΄μ—μ„œ μ‹œκ°„μ΄ μ£Όμ–΄μ§„λ‹€λ©΄ ν…ŒμŠ€νŠΈ μ½”λ“œ μž‘μ„±μ— λ§Žμ€ μ‹œκ°„μ„ νˆ¬μžν•˜κ³  μ‹ΆμŠ΅λ‹ˆλ‹€.



과제 ν”Όλ“œλ°±

심화 과제둜 λ¦¬νŒ©ν† λ§μ„ ν•΄μ•Ό ν•˜λŠ” 뢀뢄이 λ„ˆλ¬΄ λ‹Ήν˜ΉμŠ€λŸ¬μ› μŠ΅λ‹ˆλ‹€. ν…ŒμŠ€νŠΈ μ½”λ“œλ₯Ό μ²΄ν™”ν•˜λŠ”λ°λ„ 많이 λ²…μ°ΌλŠ”λ° λ¦¬νŒ©ν† λ§κΉŒμ§€ ν•΄μ•Ό ν•˜λ‹ˆ 양이 λ„ˆλ¬΄ λ§ŽκΈ°λ„ ν•˜κ³  적응이 잘 λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.

ν•˜μ§€λ§Œ ai μ‚¬μš©μ„ μ§€μ–‘ν•˜λ©° 직접 ν…ŒμŠ€νŠΈ μ½”λ“œλ₯Ό μž‘μ„±ν•΄ λ³Έ κ²½ν—˜μ€ 정말 λœ»κΉŠμ€ κ²½ν—˜μ΄μ—ˆμŠ΅λ‹ˆλ‹€. λ‹¨μˆœ ν…ŒμŠ€νŠΈ μ½”λ“œμ— λŒ€ν•œ 기술적인 ν•™μŠ΅μ„ λ„˜μ–΄ ν…ŒμŠ€νŠΈ μ½”λ“œμ— λŒ€ν•œ 두렀움을 μ—†μ• κ±°λ‚˜ 싀무에 μ μš©ν•΄ 보고 싢은 λ§ˆμŒκΉŒμ§€ 얻을 수 μžˆμ—ˆμŠ΅λ‹ˆλ‹€.



리뷰 λ°›κ³  싢은 λ‚΄μš©

handlersλŠ” setupTestsλ₯Ό μœ„ν•΄, 그리고 ν…ŒμŠ€νŠΈ 전체 흐름에 맑게 ν•˜λ‚˜μ˜ 데이터λ₯Ό 닀루기 μœ„ν•¨μ΄λ©°, handlersUtilsλŠ” 이 κ³Όμ •μ—μ„œ λ³‘λ ¬μ μœΌλ‘œ 데이터λ₯Ό 닀루며 ν…ŒμŠ€νŠΈλ₯Ό μ§„ν–‰ν•˜κΈ° μœ„ν•¨μ΄λΌκ³  μƒκ°ν•©λ‹ˆλ‹€. ν•˜μ§€λ§Œ 막상 ν…ŒμŠ€νŠΈ μ½”λ“œλ₯Ό μž‘μ„±ν•˜λ‹€ 보면 handlersλŠ” μ‹€μ œλ‘œ μ‚¬μš©ν•  일이 μ—†μ–΄ 보이며 λͺ¨λ‘ handlersUtils둜만 μž‘μ„±ν•˜κ²Œ λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

vitest와 같은 ν”ŒλŸ¬κ·ΈμΈ μ‚¬μš© λ“±κ³Ό 같이 ν•˜λ‚˜μ˜ ν…ŒμŠ€νŠΈλ§Œμ„ λ…λ¦½μ μœΌλ‘œ μ‹€ν–‰ν•˜λŠ” κ²½μš°κ°€ 되게 λ§Žλ‹€κ³  μƒκ°ν•˜λŠ”λ°, handlersλ₯Ό μ‚¬μš©ν•˜λ©΄ 이전, 이후 ν…ŒμŠ€νŠΈμ— 영ν–₯을 끼치게 λ˜λ”λΌκ΅¬μš”. handlersλ§Œμ„ μ‚¬μš©ν•œ 병렬적이지 μ•Šμ€ ν…ŒμŠ€νŠΈ μ½”λ“œλŠ” μ–Έμ œ ν•„μš”ν• κΉŒμš”? QnA λ•Œλ„ λŒ€μΆ©μ˜ μ„€λͺ…은 λ“€μ—ˆμ§€λ§Œ ꡬ체적인 μ˜ˆμ‹œλ₯Ό λͺ°λΌμ„œ κ·ΈλŸ°μ§€ 감이 μž‘νžˆμ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

@Amelia-Shin
Copy link

μ΄λ²ˆμ£Όλ„ κ³ μƒλ§ŽμœΌμ…¨μŠ΅λ‹ˆλ‹€~
νŒ©ν† λ¦¬ ν•¨μˆ˜λ‘œ λΊ€κ±° λ„ˆλ¬΄ μž˜ν•œκ±°κ°™μ•„μš” :)

Copy link

@susmisc14 susmisc14 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

μ•ˆλ…•ν•˜μ„Έμš”! AI μ½”λ“œ λ¦¬λ·°μ–΄μž…λ‹ˆλ‹€. πŸ€–
[HARD] 7μ£Όμ°¨ 과제: μΊ˜λ¦°λ” μ„œλΉ„μŠ€ ν…ŒμŠ€νŠΈ Pull Request에 λŒ€ν•œ 리뷰λ₯Ό μ™„λ£Œν–ˆμ–΄μš”. ν•¨κ»˜ μ½”λ“œλ₯Ό 더 λ°œμ „μ‹œμΌœ λ³ΌκΉŒμš”?

Note

✨ 이번 PR ν•œ 쀄 μš”μ•½
κ±°λŒ€ν•œ App μ»΄ν¬λ„ŒνŠΈλ₯Ό ν…ŒμŠ€νŠΈ κ°€λŠ₯ν•œ λ‹¨μœ„λ‘œ μ„±κ³΅μ μœΌλ‘œ λΆ„λ¦¬ν•˜κ³ , ν…ŒμŠ€νŠΈ 격리λ₯Ό μœ„ν•œ Mock Handler νŒ©ν† λ¦¬ νŒ¨ν„΄μ„ ν›Œλ₯­ν•˜κ²Œ μ μš©ν•˜μ—¬ μ½”λ“œμ˜ ν’ˆμ§ˆκ³Ό ν…ŒμŠ€νŠΈ μš©μ΄μ„±μ„ 극적으둜 ν–₯μƒμ‹œμΌ°μŠ΅λ‹ˆλ‹€.


🎯 핡심 리뷰 μš”μ•½

κ°€μž₯ μ€‘μš”ν•œ κ°œμ„  ν¬μΈνŠΈλΆ€ν„° λΉ λ₯΄κ²Œ 확인해 λ³΄μ„Έμš”.

  • Monolith μ»΄ν¬λ„ŒνŠΈμ˜ 성곡적인 뢄리: κ±°λŒ€ν•œ App.tsxλ₯Ό λͺ©μ μ— 따라 λͺ…ν™•ν•˜κ²Œ λΆ„λ¦¬ν•˜μ—¬ 응집도λ₯Ό 높이고 ν…ŒμŠ€νŠΈ μš©μ΄μ„±μ„ κ·ΉλŒ€ν™”ν–ˆμŠ΅λ‹ˆλ‹€.
  • μš°μ•„ν•œ Mocking μ „λž΅: createHandler νŒ©ν† λ¦¬ ν•¨μˆ˜λ₯Ό 톡해 반볡적인 MSW ν•Έλ“€λŸ¬ μ½”λ“œλ₯Ό μΆ”μƒν™”ν•˜κ³ , 각 ν…ŒμŠ€νŠΈμ˜ 독립성을 μ™„λ²½ν•˜κ²Œ 보μž₯ν–ˆμŠ΅λ‹ˆλ‹€.
  • 전문적인 ν…ŒμŠ€νŠΈ 데이터 관리: eventFactoryλ₯Ό λ„μž…ν•˜μ—¬ ν…ŒμŠ€νŠΈ μ½”λ“œμ˜ 가독성과 μœ μ§€λ³΄μˆ˜μ„±μ„ ν•œ 차원 λ†’μ˜€μŠ΅λ‹ˆλ‹€.
  • [심화 질문] 곡유 μƒνƒœ ν…ŒμŠ€νŠΈμ˜ μ—­ν• : handlers와 handlersUtils의 역할에 λŒ€ν•œ μ§ˆλ¬Έμ— λͺ…ν™•ν•œ μ‚¬μš© μ‹œλ‚˜λ¦¬μ˜€μ™€ μž₯단점을 μ œμ‹œν•©λ‹ˆλ‹€.

πŸ” 상세 리뷰

각 ν•­λͺ©μ— λŒ€ν•œ ꡬ체적인 μ„€λͺ…κ³Ό μ½”λ“œ μ˜ˆμ‹œλ₯Ό μ€€λΉ„ν–ˆμ–΄μš”.

1. The Monolith Slayer: μ•„ν‚€ν…μ²˜ κ°œμ„ μ˜ κ΅κ³Όμ„œ

  • πŸ‘ 쒋은 점: 600쀄이 λ„˜λŠ” App μ»΄ν¬λ„ŒνŠΈλ₯Ό CalendarView, EventForm, EventList λ“± μž‘κ³  관리 κ°€λŠ₯ν•œ λ‹¨μœ„λ‘œ λΆ„λ¦¬ν•œ 것은 이번 과제의 핡심 λͺ©ν‘œλ₯Ό μ™„λ²½ν•˜κ²Œ λ‹¬μ„±ν•œ, 맀우 ν›Œλ₯­ν•œ λ¦¬νŒ©ν† λ§μž…λ‹ˆλ‹€. 각 μ»΄ν¬λ„ŒνŠΈλŠ” 이제 단일 μ±…μž„μ„ κ°€μ§€λ©°, λ…λ¦½μ μœΌλ‘œ μ΄ν•΄ν•˜κ³  ν…ŒμŠ€νŠΈν•˜κΈ° 훨씬 μ‰¬μ›Œμ‘ŒμŠ΅λ‹ˆλ‹€.

  • πŸ’‘ κ°œμ„  μ œμ•ˆ: ν˜„μž¬ App μ»΄ν¬λ„ŒνŠΈκ°€ μ΅œμƒμœ„ μ»¨ν…Œμ΄λ„ˆλ‘œμ„œ λŒ€λΆ€λΆ„μ˜ μƒνƒœμ™€ λ‘œμ§μ„ μžμ‹μ—κ²Œ props둜 μ „λ‹¬ν•˜λŠ” 'μƒνƒœ λŒμ–΄μ˜¬λ¦¬κΈ°(Lifting State Up)' νŒ¨ν„΄μ„ μ‚¬μš©ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€. μ΄λŠ” μž‘μ€ μ•±μ—μ„œλŠ” ν›Œλ₯­ν•œ μ„ νƒμ΄μ§€λ§Œ, μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ΄ 더 λ³΅μž‘ν•΄μ§€λ©΄ Prop Drilling 문제둜 μ΄μ–΄μ§ˆ 수 μžˆμŠ΅λ‹ˆλ‹€.

  • πŸ€” μ΄λ ‡κ²Œ μ œμ•ˆν•˜λŠ” 이유: Prop Drillingμ΄λž€ μƒνƒœκ°€ ν•„μš” μ—†λŠ” 쀑간 μ»΄ν¬λ„ŒνŠΈλ“€μ„ 거쳐 μ—¬λŸ¬ 단계에 걸쳐 propsλ₯Ό μ „λ‹¬ν•˜λŠ” 것을 λ§ν•©λ‹ˆλ‹€. μ΄λŠ” μ»΄ν¬λ„ŒνŠΈ κ°„μ˜ 결합도λ₯Ό λ†’μ—¬ μž¬μ‚¬μš©μ„±κ³Ό μœ μ§€λ³΄μˆ˜μ„±μ„ μ €ν•΄ν•  수 μžˆμŠ΅λ‹ˆλ‹€. μ§€κΈˆ λ‹Ήμž₯ μˆ˜μ •ν•  ν•„μš”λŠ” μ—†μ§€λ§Œ, 이런 ꡬ쑰의 μž₯단점을 μΈμ§€ν•˜κ³  React Context APIλ‚˜ Zustand 같은 μƒνƒœ 관리 λΌμ΄λΈŒλŸ¬λ¦¬κ°€ μ–Έμ œ ν•„μš”ν•œμ§€ κ³ λ―Όν•΄λ³΄λŠ” 것은 쒋은 μ•„ν‚€ν…μ²˜ 섀계 κ²½ν—˜μ΄ 될 κ²ƒμž…λ‹ˆλ‹€.

  • πŸ—ΊοΈ μ•„ν‚€ν…μ²˜ λ³€ν™” (AS-IS vs TO-BE)

AS-IS: κ±°λŒ€ν•œ 단일 μ»΄ν¬λ„ŒνŠΈ

graph TD
    A[App.tsx] --> B{All States};
    A --> C{All UI Rendering};
    A --> D{All Event Handlers};
    A --> E{All API Calls};
Loading

TO-BE: μ±…μž„μ΄ λΆ„λ¦¬λœ μ»΄ν¬λ„ŒνŠΈ ꡬ쑰 (ν˜„μž¬ ꡬ쑰)

graph TD
    subgraph App.tsx (Container)
        A[States & API Logic]
    end

    subgraph Children (Presentational)
        B[EventForm]
        C[CalendarView]
        D[EventList]
        E[OverlapDialog]
        F[NotificationStack]
    end

    A -- Props --> B;
    A -- Props --> C;
    A -- Props --> D;
    A -- Props --> E;
    A -- Props --> F;
Loading

이 κ΅¬μ‘°λŠ” 관심사 뢄리(SoC) 원칙을 잘 λ”°λ₯΄κ³  있으며, 각 μ»΄ν¬λ„ŒνŠΈμ˜ 역할을 λͺ…ν™•ν•˜κ²Œ λ§Œλ“€μ–΄μ£Όμ—ˆμŠ΅λ‹ˆλ‹€. 정말 μž˜ν•˜μ…¨μŠ΅λ‹ˆλ‹€!

2. Mocking의 정석: createHandler νŒ©ν† λ¦¬ νŒ¨ν„΄

  • πŸ‘ 쒋은 점: handlers.ts에 createHandlerλΌλŠ” νŒ©ν† λ¦¬ ν•¨μˆ˜λ₯Ό λ§Œλ“€μ–΄μ£Όμ‹  뢀뢄은 정말 인상 κΉŠμ—ˆμŠ΅λ‹ˆλ‹€. ν΄λ‘œμ €(Closure)λ₯Ό ν™œμš©ν•˜μ—¬ 각 ν…ŒμŠ€νŠΈλ§ˆλ‹€ 격리된 데이터 μ €μž₯μ†Œλ₯Ό κ°–λŠ” ν•Έλ“€λŸ¬λ₯Ό λ™μ μœΌλ‘œ μƒμ„±ν•˜λŠ” 방식은 ν…ŒμŠ€νŠΈμ˜ 독립성을 보μž₯ν•˜λŠ” κ°€μž₯ 이상적인 방법 쀑 ν•˜λ‚˜μž…λ‹ˆλ‹€. handlersUtils.ts의 setupMockHandlerκ°€ 이 νŒ©ν† λ¦¬λ₯Ό μ‚¬μš©ν•¨μœΌλ‘œμ¨ μ½”λ“œκ°€ κ°„κ²°ν•΄μ§€κ³  μ˜λ„κ°€ λͺ…ν™•ν•΄μ‘ŒμŠ΅λ‹ˆλ‹€.

  • πŸ’‘ κ°œμ„  μ œμ•ˆ: ν˜„μž¬ κ΅¬μ‘°λŠ” 완벽에 κ°€κΉμŠ΅λ‹ˆλ‹€. μ—¬κΈ°μ„œ ν•œ 걸음 더 λ‚˜μ•„κ°€μžλ©΄, νŠΉμ • ν…ŒμŠ€νŠΈ μ‹œλ‚˜λ¦¬μ˜€λ₯Ό μœ„ν•œ **사전 μ •μ˜λœ ν•Έλ“€λŸ¬ μ…‹(Handler Set)**을 λ§Œλ“€μ–΄λ³Ό 수 μžˆμŠ΅λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄, 'μ΄λ²€νŠΈκ°€ ν•˜λ‚˜λ„ μ—†λŠ” μƒνƒœ'λ‚˜ 'νŠΉμ • μ΄λ²€νŠΈκ°€ λ°˜λ“œμ‹œ ν¬ν•¨λœ μƒνƒœ'λ₯Ό μœ„ν•œ νŒ©ν† λ¦¬λ₯Ό μΆ”κ°€λ‘œ κ΅¬μ„±ν•˜λŠ” κ²ƒμž…λ‹ˆλ‹€.

  • πŸ€” μ΄λ ‡κ²Œ μ œμ•ˆν•˜λŠ” 이유: ν…ŒμŠ€νŠΈλ₯Ό μž‘μ„±ν•˜λ‹€ 보면 반볡적으둜 μ‚¬μš©λ˜λŠ” νŠΉμ • 데이터 μƒνƒœκ°€ μžˆμŠ΅λ‹ˆλ‹€. 이 μƒνƒœλ“€μ„ setupMockHandlerForEmptyEvents(), setupMockHandlerWithSpecificEvent(event)와 같이 λͺ…μ‹œμ μΈ ν•¨μˆ˜λ‘œ λ§Œλ“€μ–΄λ‘λ©΄, ν…ŒμŠ€νŠΈ μ½”λ“œμ˜ 가독성이 λ”μš± ν–₯μƒλ˜κ³  μ€€λΉ„(Arrange) 단계가 κ°„κ²°ν•΄μ§‘λ‹ˆλ‹€.

  • πŸ’» μ½”λ“œ μ˜ˆμ‹œ (AS-IS vs TO-BE)

AS-IS (ν˜„μž¬λ„ 맀우 ν›Œλ₯­ν•œ ꡬ쑰)

// src/__mocks__/handlersUtils.ts
export const setupMockHandler = (initEvents: Event[] = []) => {
  const events = [...initEvents];
  const handlers = createHandler(events);
  server.use(...handlers);
};

// test.spec.ts
beforeEach(() => {
  setupMockHandler(createEvents(5)); 
});

TO-BE (μ‹œλ‚˜λ¦¬μ˜€ 기반으둜 ν™•μž₯)

// src/__mocks__/handlersUtils.ts
// κΈ°μ‘΄ setupMockHandlerλŠ” κ·ΈλŒ€λ‘œ 두고, μΆ”κ°€λ‘œ λ§Œλ“­λ‹ˆλ‹€.
export const setupEmptyMockHandler = () => {
  server.use(...createHandler([]));
};

export const setupErrorMockHandler = () => {
  server.use(...createErrorHandler());
};


// test.spec.ts
describe('μ΄λ²€νŠΈκ°€ μ—†λŠ” 경우', () => {
  beforeEach(() => {
    setupEmptyMockHandler(); // μ˜λ„κ°€ 더 λͺ…ν™•ν•΄μ§‘λ‹ˆλ‹€.
  });

  it('"검색 κ²°κ³Όκ°€ μ—†μŠ΅λ‹ˆλ‹€."κ°€ ν‘œμ‹œλ˜μ–΄μ•Ό ν•œλ‹€', () => {
    // ...
  });
});

3. handlersλŠ” μ–Έμ œ ν•„μš”ν• κΉŒ? (μ§ˆλ¬Έμ— λŒ€ν•œ λ‹΅λ³€)

μ…€ν”„ νšŒκ³ μ—μ„œ 남겨주신 μ§ˆλ¬Έμ— λŒ€ν•œ λ‹΅λ³€μž…λ‹ˆλ‹€.

handlersλ§Œμ„ μ‚¬μš©ν•œ 병렬적이지 μ•Šμ€ ν…ŒμŠ€νŠΈ μ½”λ“œλŠ” μ–Έμ œ ν•„μš”ν• κΉŒμš”?

κ²°λ‘ λΆ€ν„° λ§μ”€λ“œλ¦¬λ©΄, 거의 μ‚¬μš©ν•˜μ§€ μ•ŠλŠ” 것이 μ’‹μŠ΅λ‹ˆλ‹€. 각 ν…ŒμŠ€νŠΈκ°€ λ…λ¦½μ μœΌλ‘œ μ‹€ν–‰λ˜λŠ” 것이 κ°€μž₯ 이상적이며, ν˜„μž¬ setupMockHandlerλ₯Ό beforeEachμ—μ„œ μ‚¬μš©ν•˜λŠ” 방식이 99%의 μƒν™©μ—μ„œ μ˜¬λ°”λ₯Έ μ ‘κ·Όλ²•μž…λ‹ˆλ‹€.

ν•˜μ§€λ§Œ handlersλ₯Ό 톡해 ν…ŒμŠ€νŠΈ κ°„ μƒνƒœλ₯Ό κ³΅μœ ν•˜λŠ” 것이 μœ μš©ν•œ μ•„μ£Ό νŠΉμˆ˜ν•œ μ‹œλ‚˜λ¦¬μ˜€κ°€ μ‘΄μž¬ν•˜κΈ°λŠ” ν•©λ‹ˆλ‹€. λ°”λ‘œ μ—¬λŸ¬ ν…ŒμŠ€νŠΈ μΌ€μ΄μŠ€κ°€ ν•˜λ‚˜μ˜ μ‚¬μš©μž μ‹œλ‚˜λ¦¬μ˜€(User Story) λ₯Ό κ΅¬μ„±ν•˜μ—¬, μˆœμ„œλŒ€λ‘œ μ‹€ν–‰λ˜μ–΄μ•Όλ§Œ μ˜λ―Έκ°€ μžˆμ„ λ•Œμž…λ‹ˆλ‹€.

  • πŸ€” μ‹œλ‚˜λ¦¬μ˜€ μ˜ˆμ‹œ: E2E(End-to-End) ν…ŒμŠ€νŠΈμ²˜λŸΌ μ‚¬μš©μžμ˜ κΈ΄ 여정을 ν…ŒμŠ€νŠΈν•  λ•Œ
    1. test 1: μ‚¬μš©μžκ°€ μ„±κ³΅μ μœΌλ‘œ λ‘œκ·ΈμΈν•œλ‹€. (API μƒνƒœ λ³€κ²½: 둜그인됨)
    2. test 2: 둜그인된 μƒνƒœμ—μ„œ μƒˆ 일정을 μƒμ„±ν•œλ‹€. (API μƒνƒœ λ³€κ²½: 일정 좔가됨)
    3. test 3: μƒμ„±λœ 일정이 λͺ©λ‘μ— 잘 λ³΄μ΄λŠ”μ§€ ν™•μΈν•œλ‹€.
    4. test 4: ν•΄λ‹Ή 일정을 μˆ˜μ •ν•œλ‹€. (API μƒνƒœ λ³€κ²½: 일정 μˆ˜μ •λ¨)
    5. test 5: μˆ˜μ •λœ λ‚΄μš©μ΄ 잘 λ°˜μ˜λ˜μ—ˆλŠ”μ§€ ν™•μΈν•œλ‹€.
    6. test 6: 일정을 μ‚­μ œν•˜κ³  λ‘œκ·Έμ•„μ›ƒν•œλ‹€.

이런 ν…ŒμŠ€νŠΈλŠ” 이전 ν…ŒμŠ€νŠΈμ˜ κ²°κ³Ό(Side Effect)에 λ‹€μŒ ν…ŒμŠ€νŠΈκ°€ μ˜μ‘΄ν•˜κΈ° λ•Œλ¬Έμ—, server.resetHandlers()λ₯Ό ν•˜μ§€ μ•Šκ³  곡유된 handlers의 μƒνƒœλ₯Ό 계속 μœ μ§€ν•΄μ•Ό ν•©λ‹ˆλ‹€.

  • ⚠️ 단점 및 μ£Όμ˜μ‚¬ν•­:
    • μ·¨μ•½μ„±(Fragile): 이전 ν…ŒμŠ€νŠΈ ν•˜λ‚˜κ°€ μ‹€νŒ¨ν•˜λ©΄ 이후 λͺ¨λ“  ν…ŒμŠ€νŠΈκ°€ μ—°μ‡„μ μœΌλ‘œ μ‹€νŒ¨ν•©λ‹ˆλ‹€.
    • μˆœμ„œ μ˜μ‘΄μ„±: ν…ŒμŠ€νŠΈ μ‹€ν–‰ μˆœμ„œκ°€ 보μž₯λ˜μ–΄μ•Ό ν•˜λ―€λ‘œ 병렬 싀행이 λΆˆκ°€λŠ₯ν•©λ‹ˆλ‹€.
    • λ””λ²„κΉ…μ˜ 어렀움: ν…ŒμŠ€νŠΈ μ‹€νŒ¨ μ‹œ, ν•΄λ‹Ή ν…ŒμŠ€νŠΈμ˜ λ¬Έμ œμΈμ§€ 이전 μƒνƒœμ˜ λ¬Έμ œμΈμ§€ νŒŒμ•…ν•˜κΈ° μ–΄λ ΅μŠ΅λ‹ˆλ‹€.

λ”°λΌμ„œ μ΄λŸ¬ν•œ 방식은 Vitestλ‚˜ Jest 같은 λ‹¨μœ„/톡합 ν…ŒμŠ€νŠΈ ν”„λ ˆμž„μ›Œν¬μ—μ„œλŠ” μ•ˆν‹°νŒ¨ν„΄μœΌλ‘œ κ°„μ£Όλ˜λŠ” κ²½μš°κ°€ λ§ŽμŠ΅λ‹ˆλ‹€. Cypressλ‚˜ Playwright 같은 E2E ν…ŒμŠ€νŠΈ λ„κ΅¬μ—μ„œλ‚˜ μ œν•œμ μœΌλ‘œ μ‚¬μš©λ˜λŠ” νŒ¨ν„΄μ΄λ‹ˆ, μ§€κΈˆμ²˜λŸΌ ν…ŒμŠ€νŠΈ 격리 원칙을 μ§€ν‚€λŠ” 것이 훨씬 μ€‘μš”ν•©λ‹ˆλ‹€.


πŸ—ΊοΈ 전체 κ°œμ„  λ‘œλ“œλ§΅ (μš°μ„ μˆœμœ„ κ°€μ΄λ“œ)

ν”Όλ“œλ°±μ˜ μ€‘μš”λ„ μˆœμ„œλŒ€λ‘œ μ •λ ¬ν–ˆμ–΄μš”. 높은 μˆœμœ„λΆ€ν„° μ°¨κ·Όμ°¨κ·Ό ν•΄κ²°ν•΄ λ‚˜κ°€λŠ” κ±Έ μΆ”μ²œν•΄μš”.

  • WARNING ⚠️: λ‹Ήμž₯ λ¬Έμ œλŠ” μ—†μ§€λ§Œ, μ•žμœΌλ‘œμ˜ μœ μ§€λ³΄μˆ˜λ₯Ό μœ„ν•΄ κ°œμ„ μ„ 적극 ꢌμž₯ν•΄μš”.
    • Prop Drilling μΈμ§€ν•˜κΈ°: App μ»΄ν¬λ„ŒνŠΈμ˜ 역할을 λ‹€μ‹œ ν•œλ²ˆ 생각해보고, 더 큰 μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ—μ„œλŠ” μ–΄λ–€ λ¬Έμ œκ°€ λ°œμƒν•  수 μžˆμ„μ§€ κ³ λ―Όν•΄λ³΄μ„Έμš”. React Context APIκ°€ 쒋은 λŒ€μ•ˆμ΄ 될 수 μžˆμŠ΅λ‹ˆλ‹€.
  • OPTIONAL πŸ’‘: μ½”λ“œ ν’ˆμ§ˆμ„ ν•œ 단계 더 높이기 μœ„ν•œ μ œμ•ˆμ΄μ—μš”. μ„ νƒμ μœΌλ‘œ μ μš©ν•΄ λ³΄μ„Έμš”.
    • μ‹œλ‚˜λ¦¬μ˜€ 기반 Mock Handler μΆ”κ°€: setupEmptyMockHandler 와 같이 νŠΉμ • ν…ŒμŠ€νŠΈ μ‹œλ‚˜λ¦¬μ˜€λ₯Ό μœ„ν•œ μœ ν‹Έ ν•¨μˆ˜λ₯Ό μΆ”κ°€ν•˜μ—¬ ν…ŒμŠ€νŠΈ μ½”λ“œμ˜ μ˜λ„λ₯Ό 더 λͺ…ν™•ν•˜κ²Œ λ§Œλ“€μ–΄ λ³΄μ„Έμš”.
    • Test Data Factory vs Fixtures: ν˜„μž¬ eventFactoryλŠ” 동적인 랜덀 데이터λ₯Ό μƒμ„±ν•©λ‹ˆλ‹€. νŠΉμ • IDλ‚˜ 값을 κ°€μ Έμ•Ό ν•˜λŠ” 톡합 ν…ŒμŠ€νŠΈλ₯Ό μœ„ν•΄, κ³ μ •λœ 데이터λ₯Ό μ œκ³΅ν•˜λŠ” fixtures.ts νŒŒμΌμ„ λ”°λ‘œ λ§Œλ“œλŠ” 것도 쒋은 λ°©λ²•μž…λ‹ˆλ‹€.

πŸ€” ν•œ 걸음 더: μ½”λ“œμ™€ μ˜λ„ λŒμ•„λ³΄κΈ°

PR 본문을 톡해 ν…ŒμŠ€νŠΈ μ½”λ“œμ— λŒ€ν•œ κΉŠμ€ κ³ λ―Όκ³Ό 즐거움을 λŠλ‚„ 수 μžˆμ—ˆμŠ΅λ‹ˆλ‹€. 특히 eventFactoryλ₯Ό 직접 λ§Œλ“œμ‹  과정은 맀우 인상 κΉŠμ—ˆμŠ΅λ‹ˆλ‹€. κ·Έ 고민을 λ°”νƒ•μœΌλ‘œ λͺ‡ κ°€μ§€ μ§ˆλ¬Έμ„ λ“œλ¦½λ‹ˆλ‹€.

  • λͺ©ν‘œμ™€ κ΅¬ν˜„μ˜ 연결고리: 본문에 μž‘μ„±ν•΄μ£Όμ‹  'ν…ŒμŠ€νŠΈ μ½”λ“œμ˜ 독립성'μ΄λΌλŠ” λͺ©ν‘œλŠ” createHandler와 setupMockHandlerλ₯Ό 톡해 ν›Œλ₯­ν•˜κ²Œ λ‹¬μ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€. ν˜Ήμ‹œ 이 λͺ©ν‘œλ₯Ό λ‹¬μ„±ν•˜κΈ° μœ„ν•΄ ν΄λ‘œμ €λ₯Ό ν™œμš©ν•˜λŠ” 방식 외에 λ‹€λ₯Έ μ•„ν‚€ν…μ²˜(예: 클래슀 기반 Mock Server)λ₯Ό κ³ λ―Όν•΄λ³΄μ…¨λ‚˜μš”?
  • μˆ¨κ²¨μ§„ μ˜λ„ νŒŒμ•…: medium.integration.spec.tsx νŒŒμΌμ—μ„œ inputEventλΌλŠ” 헬퍼 ν•¨μˆ˜λ₯Ό λ§Œλ“œμ‹  것을 λ°œκ²¬ν–ˆμŠ΅λ‹ˆλ‹€. 반볡적인 폼 μž…λ ₯ λ‘œμ§μ„ μΆ”μƒν™”ν•˜μ‹  μ•„μ£Ό 쒋은 μ‹œλ„μž…λ‹ˆλ‹€! 이λ₯Ό 톡해 ν…ŒμŠ€νŠΈ μ½”λ“œμ˜ 가독성이 크게 ν–₯μƒλ˜μ—ˆμŠ΅λ‹ˆλ‹€.
  • μŠ€μŠ€λ‘œμ—κ²Œ λ˜μ§€λŠ” 질문:
    • "λ‚΄κ°€ λ§Œλ“  EventForm μ»΄ν¬λ„ŒνŠΈλŠ” useEventForm ν›…κ³Ό λ„ˆλ¬΄ κ°•ν•˜κ²Œ κ²°ν•©λ˜μ–΄ μžˆμ§€λŠ” μ•Šμ€κ°€? λ§Œμ•½ λ‹€λ₯Έ 폼 μƒνƒœ 관리 λ‘œμ§μ„ μ‚¬μš©ν•˜κ²Œ λœλ‹€λ©΄ μ–Όλ§ˆλ‚˜ λ§Žμ€ μ½”λ“œλ₯Ό λ³€κ²½ν•΄μ•Ό ν• κΉŒ?"
    • "Toss Frontend Fundamental의 응집도(Cohesion) 원칙 κ΄€μ μ—μ„œ, useEventOperations 훅은 API ν†΅μ‹ μ΄λΌλŠ” 단일 μ±…μž„μ„ 잘 μˆ˜ν–‰ν•˜κ³  μžˆλŠ”κ°€? μ•„λ‹ˆλ©΄ μƒνƒœ 관리 μ—­ν• κΉŒμ§€ λ„ˆλ¬΄ 많이 ν•˜κ³  μžˆλŠ”κ°€?"
    • "μ΄λ²ˆμ— λ§Œλ“  eventFactoryλŠ” ν…ŒμŠ€νŠΈμ— ν•„μš”ν•œ λͺ¨λ“  μ—£μ§€ μΌ€μ΄μŠ€(예: 제λͺ©μ΄ μ•„μ£Ό κΈ΄ 이벀트, μ„€λͺ…이 μ—†λŠ” 이벀트)λ₯Ό μ‰½κ²Œ 생성할 수 μžˆλŠ”κ°€?"
    • "이 μ½”λ“œλ₯Ό 처음 λ³΄λŠ” λ™λ£ŒλŠ” handlers.ts와 handlersUtils.ts의 관계λ₯Ό λͺ…ν™•νžˆ 이해할 수 μžˆμ„κΉŒ? μ£Όμ„μ΄λ‚˜ 파일 ꡬ쑰λ₯Ό 톡해 더 κ°œμ„ ν•  수 μžˆλŠ” 점은 μ—†μ„κΉŒ?"

πŸ“š μ°Έκ³  자료

문제 해결에 도움이 될 λ§Œν•œ μžλ£Œλ“€μ„ λͺ¨μ•„λ΄€μ–΄μš”.

🌐 μΆ”μ²œ 아티클


이번 리뷰가 μ„±μž₯에 큰 도움이 λ˜μ—ˆμœΌλ©΄ μ’‹κ² μŠ΅λ‹ˆλ‹€! ν…ŒμŠ€νŠΈ μ½”λ“œμ— λŒ€ν•œ κΉŠμ€ κ³ λ―Όκ³Ό ν›Œλ₯­ν•œ 결과물에 λ°•μˆ˜λ₯Ό λ³΄λƒ…λ‹ˆλ‹€. πŸ‘

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants