jun17183 ๋‹˜์˜ ์ƒ์„ธํŽ˜์ด์ง€ ๏ผž [9ํŒ€ ์‹ ํ™์ค€] Chapter ๐Ÿฆ 3-2. ํ”„๋ก ํŠธ์—”๋“œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ๐Ÿฆ๐Ÿฆ๐Ÿฆ

8์ฃผ์ฐจ ๊ณผ์ œ ์ฒดํฌํฌ์ธํŠธ

๊ธฐ๋ณธ ๊ณผ์ œ

ํ•„์ˆ˜

  • ๋ฐ˜๋ณต ์œ ํ˜• ์„ ํƒ
    • ์ผ์ • ์ƒ์„ฑ ๋˜๋Š” ์ˆ˜์ • ์‹œ ๋ฐ˜๋ณต ์œ ํ˜•์„ ์„ ํƒํ•  ์ˆ˜ ์žˆ๋‹ค.
    • ๋ฐ˜๋ณต ์œ ํ˜•์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค: ๋งค์ผ, ๋งค์ฃผ, ๋งค์›”, ๋งค๋…„
      • 31์ผ์— ๋งค์›”์„ ์„ ํƒํ•œ๋‹ค๋ฉด -> ๋งค์›” ๋งˆ์ง€๋ง‰์ด ์•„๋‹Œ, 31์ผ์—๋งŒ ์ƒ์„ฑํ•˜์„ธ์š”.
      • ์œค๋…„ 29์ผ์— ๋งค๋…„์„ ์„ ํƒํ•œ๋‹ค๋ฉด -> 29์ผ์—๋งŒ ์ƒ์„ฑํ•˜์„ธ์š”!
  • ๋ฐ˜๋ณต ์ผ์ • ํ‘œ์‹œ
    • ์บ˜๋ฆฐ๋” ๋ทฐ์—์„œ ๋ฐ˜๋ณต ์ผ์ •์„ ์‹œ๊ฐ์ ์œผ๋กœ ๊ตฌ๋ถ„ํ•˜์—ฌ ํ‘œ์‹œํ•œ๋‹ค.
      • ์•„์ด์ฝ˜์„ ๋„ฃ๋“  ํƒœ๊ทธ๋ฅผ ๋„ฃ๋“  ์ž์œ ๋กญ๊ฒŒ ํ•ด๋ณด์„ธ์š”!
  • ๋ฐ˜๋ณต ์ข…๋ฃŒ
    • ๋ฐ˜๋ณต ์ข…๋ฃŒ ์กฐ๊ฑด์„ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.
    • ์˜ต์…˜: ํŠน์ • ๋‚ ์งœ๊นŒ์ง€, ํŠน์ • ํšŸ์ˆ˜๋งŒํผ, ๋˜๋Š” ์ข…๋ฃŒ ์—†์Œ (์˜ˆ์ œ ํŠน์„ฑ์ƒ, 2025-06-30๊นŒ์ง€)
  • ๋ฐ˜๋ณต ์ผ์ • ๋‹จ์ผ ์ˆ˜์ •
    • ๋ฐ˜๋ณต์ผ์ •์„ ์ˆ˜์ •ํ•˜๋ฉด ๋‹จ์ผ ์ผ์ •์œผ๋กœ ๋ณ€๊ฒฝ๋ฉ๋‹ˆ๋‹ค.
    • ๋ฐ˜๋ณต์ผ์ • ์•„์ด์ฝ˜๋„ ์‚ฌ๋ผ์ง‘๋‹ˆ๋‹ค.
  • ๋ฐ˜๋ณต ์ผ์ • ๋‹จ์ผ ์‚ญ์ œ
    • ๋ฐ˜๋ณต์ผ์ •์„ ์‚ญ์ œํ•˜๋ฉด ํ•ด๋‹น ์ผ์ •๋งŒ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค.

์„ ํƒ

  • ๋ฐ˜๋ณต ๊ฐ„๊ฒฉ ์„ค์ •
    • ๊ฐ ๋ฐ˜๋ณต ์œ ํ˜•์— ๋Œ€ํ•ด ๊ฐ„๊ฒฉ์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.
    • ์˜ˆ: 2์ผ๋งˆ๋‹ค, 3์ฃผ๋งˆ๋‹ค, 2๊ฐœ์›”๋งˆ๋‹ค ๋“ฑ
  • ์˜ˆ์™ธ ๋‚ ์งœ ์ฒ˜๋ฆฌ:
    • ๋ฐ˜๋ณต ์ผ์ • ์ค‘ ํŠน์ • ๋‚ ์งœ๋ฅผ ์ œ์™ธํ•  ์ˆ˜ ์žˆ๋‹ค.
    • ๋ฐ˜๋ณต ์ผ์ • ์ค‘ ํŠน์ • ๋‚ ์งœ์˜ ์ผ์ •์„ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ์š”์ผ ์ง€์ • (์ฃผ๊ฐ„ ๋ฐ˜๋ณต์˜ ๊ฒฝ์šฐ):
    • ์ฃผ๊ฐ„ ๋ฐ˜๋ณต ์‹œ ํŠน์ • ์š”์ผ์„ ์„ ํƒํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ์›”๊ฐ„ ๋ฐ˜๋ณต ์˜ต์…˜:
    • ๋งค์›” ํŠน์ • ๋‚ ์งœ์— ๋ฐ˜๋ณต๋˜๋„๋ก ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.
    • ๋งค์›” ํŠน์ • ์ˆœ์„œ์˜ ์š”์ผ์— ๋ฐ˜๋ณต๋˜๋„๋ก ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ๋ฐ˜๋ณต ์ผ์ • ์ „์ฒด ์ˆ˜์ • ๋ฐ ์‚ญ์ œ
    • ๋ฐ˜๋ณต ์ผ์ •์˜ ๋ชจ๋“  ์ผ์ •์„ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.
    • ๋ฐ˜๋ณต ์ผ์ •์˜ ๋ชจ๋“  ์ผ์ •์„ ์‚ญ์ œํ•  ์ˆ˜ ์žˆ๋‹ค.

์‹ฌํ™” ๊ณผ์ œ

  • ์ด ์•ฑ์— ์ ํ•ฉํ•œ ํ…Œ์ŠคํŠธ ์ „๋žต์„ ๋งŒ๋“ค์—ˆ๋‚˜์š”?

๊ฐ ํŒ€์›๋“ค์˜ ํ…Œ์ŠคํŠธ ์ „๋žต์€?

์ €ํฌํŒ€์€ ํŒ€์›๋ณ„๋กœ ํ…Œ์ŠคํŠธ ์ „๋žต์— ๋Œ€ํ•œ ์˜๊ฒฌ์„ ์ฃผ๊ณ  ๋ฐ›๊ธฐ ๋ณด๋‹ค๋Š” ํ”ผ๊ทธ์žผ์„ ํ†ตํ•ด ํ•จ๊ป˜ ๋ธŒ๋ ˆ์ธ์Šคํ† ๋ฐํ•˜๋ฉฐ ์ „๋žต์„ ์ขํ˜€ ๋‚˜๊ฐ”์Šต๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ, ํ•˜๋‚˜์˜ PR์— ํ…Œ์ŠคํŠธ ์ „๋žต์„ ์ˆ˜๋ฆฝํ•˜์—ฌ ๋ชจ๋‘ ํŽ˜์–ด์ฝ”๋”ฉ์„ ์ง„ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ฒฐ๋ก ์ ์œผ๋กœ 9ํŒ€์€ ์‹ฌํ™”๊ณผ์ œ PR๋ถ€๋ถ„๊ณผ ์ฝ”๋“œ ์ „๋žต์„ ํ•จ๊ป˜ ์ž‘์„ฑํ•˜์—ฌ ๊ทธ ๋‚ด์šฉ์ด ๊ฐ™์Šต๋‹ˆ๋‹ค.

ํ•ฉ์˜๋œ ํ…Œ์ŠคํŠธ ์ „๋žต๊ณผ ๊ทธ ์ด์œ ๋Š” ๋ฌด์—‡์ธ๊ฐ€์š”?

์ €ํฌ 9ํŒ€์€ TDD ๊ธฐ๋ฐ˜์œผ๋กœ ๋ฐ˜๋ณต ์ผ์ • ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๋ฉด์„œ ์œค๋…„, ์›”๋ง ๋‚ ์งœ ๋“ฑ ๋‹ค์–‘ํ•œ ์—ฃ์ง€ ์ผ€์ด์Šค๋ฅผ ๋งˆ์ฃผํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ  ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ํ…Œ์ŠคํŠธ ์ „๋žต์˜ ๋ชฉํ‘œ๋ฅผ ์—ฃ์ง€ ์ผ€์ด์Šค๋ฅผ ์ตœ์†Œํ™”ํ•˜๋Š” ๊ฒƒ์œผ๋กœ ์žก๊ณ , ์ด๋ฅผ ๊ฒ€์ฆํ•˜๊ธฐ ์œ„ํ•œ ๋ฐฉํ–ฅ์„ ๊ณ ๋ฏผํ–ˆ์Šต๋‹ˆ๋‹ค.

์ด ๊ณผ์ •์—์„œ ๋‹จ์ˆœํžˆ โ€œ์–ด๋–ป๊ฒŒ ํ…Œ์ŠคํŠธํ• ๊นŒโ€ ๋ผ๋Š” ๊ด€์ ์— ๋จธ๋ฌด๋ฅด์ง€ ์•Š๊ณ , โ€œ์–ด๋–ป๊ฒŒ ํ•˜๋ฉด ์ฝ”๋“œ๋ฅผ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์ข‹์€ ๊ตฌ์กฐ๋กœ ๋งŒ๋“ค๊นŒโ€ ๋ผ๋Š” ์งˆ๋ฌธ์— ์ง‘์ค‘ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ฝ”์น˜๋‹˜๊ป˜์„œ๋„ ์ด ์ ์„ ๊ฐ•์กฐํ•ด์ฃผ์…จ๊ณ , ๊ทธ ์กฐ์–ธ์„ ๋ฐ”ํƒ•์œผ๋กœ ๋ ˆ์ด์–ด๋ฅผ ๋จผ์ € ์ถ”์ƒํ™”ํ•œ ๋’ค, ํ…Œ์ŠคํŠธํ•  ๋ถ€๋ถ„๊ณผ ๊ทธ๋ ‡์ง€ ์•Š์€ ๋ถ€๋ถ„์„ ์˜๋„์ ์œผ๋กœ ๊ตฌ๋ถ„ํ•˜์—ฌ ์„ ํƒ์ ์œผ๋กœ ๊ฒ€์ฆํ•˜๋Š” ์ „๋žต์„ ์ˆ˜๋ฆฝํ–ˆ์Šต๋‹ˆ๋‹ค.

์•„ํ‚คํ…์ฒ˜ ์ •์˜ํ•˜๊ธฐ

[๊ธฐ์กด ๊ตฌ์กฐ]

  • ์—ญํ•  ๊ธฐ๋ฐ˜ ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ
  • ์œ ๋‹› ํ…Œ์ŠคํŠธ ๋น„์ค‘์ด ๋†’์Œ (ํ”ผ๋ผ๋ฏธ๋“œ ๊ตฌ์„ฑ)

๋ฟ๋งŒ ์•„๋‹ˆ๋ผ utils ๋””๋ ‰ํ† ๋ฆฌ๋Š” ์•„๋ž˜์™€ ๊ฐ™์€ ์„œ๋กœ ๋‹ค๋ฅธ ์—ญํ• ๊ณผ ๊ด€์‹ฌ์‚ฌ๊ฐ€ ํ˜ผ์žฌํ•˜๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

  • repeatEventUtils : ๋ฐ˜๋ณต ์ผ์ •์˜ ๋‹ค์Œ ๋‚ ์งœ๋ฅผ ๊ณ„์‚ฐํ•˜๋Š” ์ˆœ์ˆ˜ ๊ณ„์‚ฐ ๋กœ์ง
  • notificationUtils : ์‚ฌ์šฉ์ž์—๊ฒŒ ์•Œ๋ฆผ์„ ๋„์šฐ๋Š” UI ๊ด€๋ จ ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ
  • eventUpdateUtils: ์ด๋ฒคํŠธ ๊ด€๋ จ ๋„๋ฉ”์ธ ๋ ˆ์ด์–ด

์ด์ฒ˜๋Ÿผ ์—ญํ• ์ด ๋‹ค๋ฅธ ์ฝ”๋“œ๋“ค์ด ํ•œ ํด๋”์— ์„ž์—ฌ์žˆ์–ด ๋ฐ˜๋ณต ์ผ์ • ๊ณ„์‚ฐ ๋กœ์ง์„ ๊ตฌ๋ณ„ํ•˜๊ธฐ ์–ด๋ ค์› ์Šต๋‹ˆ๋‹ค.


[๋ ˆ์ด์–ด ๋„์ถœ]

  • ๋ฐ˜๋ณต ์ผ์ •์˜ ์—ฃ์ง€ ์ผ€์ด์Šค๋ฅผ ํŒ๋‹จํ•˜๋Š” ํ•ต์‹ฌ ๋กœ์ง์€ ๋Œ€๋ถ€๋ถ„ repeatEventUtils ๊ฐ™์€ ์ˆœ์ˆ˜ ํ•จ์ˆ˜ ์•ˆ์— ์žˆ๋‹ค๊ณ  ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค.
  • ์ด ํ•จ์ˆ˜๋“ค์„ ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ๊ฐ€ ์žˆ๋Š” ํ•จ์ˆ˜๋กœ๋ถ€ํ„ฐ ๋ถ„๋ฆฌํ•˜์—ฌ ๋ ˆ์ด์–ด๋ฅผ ๋‚˜๋ˆˆ๋‹ค๋ฉด ํ…Œ์ŠคํŠธ๊ฐ€ ์‰ฌ์›Œ์งˆ ๊ฒƒ์ด๋ผ๊ณ  ์˜๊ฒฌ์„ ๋ชจ์•˜์Šต๋‹ˆ๋‹ค. ์ด ๋ถ€๋ถ„์„ ๋„๋ฉ”์ธ ์œ ํ‹ธ ๋ ˆ์ด์–ด(repeat) ๋กœ ์ •์˜ํ•˜๊ณ  ๊ธฐ์กด์— ์ž‘์„ฑํ•œ ์œ ๋‹› ํ…Œ์ŠคํŠธ๋ฅผ ํ™œ์šฉํ•˜๊ธฐ๋กœ ํ–ˆ์Šต๋‹ˆ๋‹ค.
  • ์ „๋žต์  ์„ ํƒ๊ณผ ์ง‘์ค‘
    • ์ง‘์ค‘: "๋„๋ฉ”์ธ ๋ ˆ์ด์–ด" ์— ์œ ๋‹› ํ…Œ์ŠคํŠธ๋ฅผ ์ง‘์ค‘ํ•˜๊ธฐ๋กœ ํ–ˆ์Šต๋‹ˆ๋‹ค.
    • ์„ ํƒ: ๋„๋ฉ”์ธ ๋กœ์ง ํ…Œ์ŠคํŠธ์— ์ง‘์ค‘ํ•˜๋Š” ๋Œ€์‹ , ๋ณ€๊ฒฝ์ด ์žฆ๊ณ  ํ…Œ์ŠคํŠธ ๋น„์šฉ์ด ๋น„์‹ผ UI ๊ด€๋ จ ํ…Œ์ŠคํŠธ์˜ ๋น„์ค‘์€ ๋‚ฎ์ถ”๊ธฐ๋กœ ๊ฒฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
    • ๊ฐ€์ •: msw ํ•ธ๋“ค๋Ÿฌ์™€ ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋‚˜ Date ๊ฐ์ฒด, Javascript ๋‚ด์žฅ ํ•จ์ˆ˜ ๊ด€๋ จ ๋กœ์ง์€ ์ž์ฒด์˜ ๋™์ž‘์„ ์‹ ๋ขฐํ•˜๊ธฐ๋กœ ๊ฐ€์ •ํ–ˆ์Šต๋‹ˆ๋‹ค. ๋Œ€์‹  API ์š”์ฒญ/์‘๋‹ต์— ๋”ฐ๋ผ ์•ฑ์ด ์–ด๋–ป๊ฒŒ ๋ฐ˜์‘ํ•˜๋Š”์ง€๋ฅผ ๊ฒ€์ฆํ•˜๋Š” ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ธฐ๋กœ ํ–ˆ์Šต๋‹ˆ๋‹ค.

[์ƒˆ๋กœ์šด ๋ ˆ์ด์–ด ๊ตฌ์กฐ] ๊ด€์‹ฌ์‚ฌ์˜ ๋ถ„๋ฆฌ์™€ ํ…Œ์ŠคํŠธ ์šฉ์ด์„ฑ์ด๋ผ๋Š” ๋‘ ๊ฐ€์ง€ ํ‚ค์›Œ๋“œ๋กœ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ ˆ์ด์–ด๋ฅผ ์„ธ๋ถ„ํ™”ํ•˜๊ณ ์ž ํ–ˆ์Šต๋‹ˆ๋‹ค.

1. utils

repeat์ด๋ผ๋Š” ๊ธฐ๋Šฅ(feature)๋‹จ์œ„๋กœ ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ๋ฌถ๊ณ , ๊ทธ ์•ˆ์—์„œ ์—ญํ• ์„ ์„ธ๋ถ„ํ™” ํ–ˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ“ฆrepeat
 โ”ฃ ๐Ÿ“œactions.ts       # ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” ๋กœ์ง (๋„๋ฉ”์ธ)
 โ”ฃ ๐Ÿ“œconstants.ts     # ๊ธฐ๋Šฅ ๊ด€๋ จ ์ƒ์ˆ˜
 โ”ฃ ๐Ÿ“œformats.ts       # ๋ฌธ์ž์—ด ํฌ๋งทํŒ… ๋“ฑ (๋„๋ฉ”์ธ/UI ๋ณด์กฐ)
 โ”— ๐Ÿ“œhelpers.ts       # ํ•ต์‹ฌ ๊ณ„์‚ฐ ๋กœ์ง (์ˆœ์ˆ˜ ํ•จ์ˆ˜, ๋„๋ฉ”์ธ)

2. test

๐Ÿ“ฆrepeat
 โ”ฃ ๐Ÿ“‚e2e
 โ”ฃ ๐Ÿ“‚integration
 โ”— ๐Ÿ“‚unit
 โ”ƒ โ”ฃ ๐Ÿ“œactions.spec.ts
 โ”ƒ โ”— ๐Ÿ“œhelpers.spec.ts

๋ ˆ์ด์–ด ๋ณ„ ํ…Œ์ŠคํŠธ ์ „๋žต ์ˆ˜๋ฆฝ

์•„ํ‚คํ…์ฒ˜๊ฐ€ ๋ช…ํ™•ํ•˜๊ฒŒ ์ •์˜๋˜๋‹ˆ ์ฝ”์น˜๋‹˜ ๋ง์”€๋Œ€๋กœ ๊ฐ ๋””๋ ‰ํ† ๋ฆฌ์˜ ์—ญํ• ์— ๋งž๋Š” ํ…Œ์ŠคํŠธ ์ „๋žต์„ ๊ตฌ์ฒดํ™” ํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

๋ฐ˜๋ณต ์ผ์ • ์ƒ์„ฑ์—์„œ๋Š” ๊ธฐ์ค€์— ๋งž๋Š” ์ผ์ • ์ƒ์„ฑ ๋ฐ ์—ฃ์ง€ ์ผ€์ด์Šค ํŒ๋‹จ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. ๋ฐ˜๋ฉด UI ๊ตฌํ˜„์˜ ๋น„์ค‘์€ ์ ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ํ…Œ์ŠคํŠธ ์ „๋žต์—๋„ ๋ฐ˜์˜ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ผ์ • ์ƒ์„ฑ๊ณผ ๊ธฐ์ค€์— ๋งž๋Š” ์ผ์ •์ผ์ง€ ํŒ๋‹จํ•˜๋Š” ํ•จ์ˆ˜์˜ ์œ ๋‹› ํ…Œ์ŠคํŠธ์˜ ๋น„์ค‘์„ ๋†’์˜€์Šต๋‹ˆ๋‹ค. UI ๊ตฌํ˜„์„ ๊ฒ€์ฆํ•˜๋Š” ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ์™€ e2eํ…Œ์ŠคํŠธ๋Š” ์ตœ์†Œํ•œ์˜ ์‚ฌ์šฉ์„ฑ์„ ๋ณด์žฅํ•˜๋Š” ์ •๋„๋กœ ์ตœ์†Œํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.

์ €ํฌ๋Š” '์ „๋žต์  ์„ ํƒ๊ณผ ์ง‘์ค‘' ์›์น™์— ๋”ฐ๋ผ ํ…Œ์ŠคํŠธ์˜ ์ข…๋ฅ˜์™€ ๋ฒ”์œ„๋ฅผ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ฒฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

[Unit Test] - ๋„๋ฉ”์ธ ๋กœ์ง (Domain Logic)

  • ๋Œ€์ƒ: utils/repeat/helpers.ts, utils/repeat/actions.ts
  • ์ „๋žต: ํ…Œ์ŠคํŠธ์˜ ์ ˆ๋ฐ˜ ์ด์ƒ์„ ์ง‘์ค‘ํ•ฉ๋‹ˆ๋‹ค. helpers์˜ ์ˆœ์ˆ˜ ๊ณ„์‚ฐ ํ•จ์ˆ˜๋“ค์„ ํ…Œ์ŠคํŠธํ•˜๋ฉฐ ์—ฃ์ง€ ์ผ€์ด์Šค๋ฅผ ๊ฒ€์ฆํ•˜๊ณ , actions์˜ ์ƒํƒœ ๋ณ€๊ฒฝ ๋กœ์ง์ด ์˜๋„๋Œ€๋กœ ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
  • ์ด์œ : ๋ฐ˜๋ณต ์ผ์ •์„ ์ƒ์„ฑํ•จ์— ์žˆ์–ด์„œ ์œค๋…„, ์›”๋ณ„ ์ผ์ˆ˜์™€ ๊ฐ™์€ ์˜ˆ์™ธ์  ์ผ€์ด์Šค๋ฅผ ๊ณ ๋ คํ•˜๋Š” ๊ฒƒ์€ ํฐ ๋น„์ค‘์„ ์ฐจ์ง€ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ์— ๋”ฐ๋ผ ํ…Œ์ŠคํŠธ์˜ ๋น„์ค‘๋„ ๊ฐ€์žฅ ๋†’์Šต๋‹ˆ๋‹ค.

[Integration Test] - ๊ณ„์ธต ๊ฐ„ ํ†ตํ•ฉ (Layer Integration)

  • ๋Œ€์ƒ: UI ์ปดํฌ๋„ŒํŠธ, API ๋ ˆ์ด์–ด, ๋„๋ฉ”์ธ ๋กœ์ง์˜ ํ†ตํ•ฉ ๋™์ž‘
  • ์ „๋žต: MSW๋ฅผ ํ™œ์šฉํ•˜์—ฌ API์˜ ์„ฑ๊ณต, ์‹คํŒจ, ๋น„์ •์ƒ ์‘๋‹ต ๋“ฑ ๋‹ค์–‘ํ•œ ์‹œ๋‚˜๋ฆฌ์˜ค์— ๋”ฐ๋ผ UI ์ƒํƒœ์™€ ๋„๋ฉ”์ธ ๋กœ์ง์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์—ฐ๋™๋˜์–ด ๋™์ž‘ํ•˜๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค.
  • ์ด์œ : "API ์š”์ฒญ/์‘๋‹ต์— ๋”ฐ๋ผ ์•ฑ์ด ์–ด๋–ป๊ฒŒ ๋ฐ˜์‘ํ•˜๋Š”๊ฐ€" ๋ฅผ ํ™•์ธํ•˜๋Š” ๊ฒƒ์ด ๋ชฉ์ ์ž…๋‹ˆ๋‹ค. ์‹ค์ œ ์„œ๋ฒ„๋‚˜ fetch ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ…Œ์ŠคํŠธ ํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ, ๊ฐ ๋ ˆ์ด์–ด๋“ค์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ํ†ตํ•ฉ๋˜์–ด ๋ฐ์ดํ„ฐ์˜ ํ๋ฆ„์„ ์ž˜ ์ฒ˜๋ฆฌํ•˜๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๋ชฉ์ ์ž…๋‹ˆ๋‹ค.

[E2E Test] - ์‚ฌ์šฉ์ž ์‹œ๋‚˜๋ฆฌ์˜ค (User Scenario)

  • ๋Œ€์ƒ: ๋ฐ˜๋ณต ์ผ์ • ํผ ๋“ฑ๋ก, ์‚ญ์ œ
  • ์ „๋žต: "์‚ฌ์šฉ์ž๊ฐ€ ๋ฐ˜๋ณต ์ผ์ •์„ ์ƒ์„ฑํ•˜๊ณ  ์ €์žฅํ•  ์ˆ˜ ์žˆ๋‹ค" ์™€ ๊ฐ™์€ ์ค‘์š”ํ•œ ์‹œ๋‚˜๋ฆฌ์˜ค ์œ„์ฃผ๋กœ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค.
  • ์ด์œ : UI๋Š” ๋ณ€๊ฒฝ์ด ์žฆ๊ณ  ํ…Œ์ŠคํŠธ ์œ ์ง€๋ณด์ˆ˜ ๋น„์šฉ์ด ์•„์ด์ฝ˜ ๋ฐ ์ด๋ฒคํŠธ ๋ฆฌ์ŠคํŠธ๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” UI๊ตฌํ˜„์˜ ์ค‘์š”๋„๊ฐ€ ์ ์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์‚ฌ์šฉ์ž์—๊ฒŒ ์ตœ์ข…์ ์œผ๋กœ ์ „๋‹ฌ๋˜๋Š” ํ•ต์‹ฌ ๊ธฐ๋Šฅ์ด ๋ฌธ์ œ์—†์ด ๋™์ž‘ํ•˜๋Š”์ง€๋งŒ์„ ๋ณด์žฅํ•˜๊ธฐ๋กœ "์„ ํƒ" ํ–ˆ์Šต๋‹ˆ๋‹ค.

์ถ”๊ฐ€๋กœ ์ž‘์„ฑ๋œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋Š” ์–ด๋–ค ๊ฒƒ๋“ค์ด ์žˆ๋‚˜์š”?

(์ž‘์„ฑ ํ•„์š”)

โ–ถ ์œ ๋‹› ํ…Œ์ŠคํŠธ

1. actions.spec.ts

  • expandEventsToNextOccurrences
    • ๋‹ค์Œ ๋‚ ์งœ์˜ ๋ฐ˜๋ณต ์ผ์ • ๋ฐ˜ํ™˜ ์—ฌ๋ถ€ ํ™•์ธ
    • weekly์˜ ๊ฒฝ์šฐ ๋‹ค์Œ ์š”์ผ, ์œค๋…„์˜ ๊ฒฝ์šฐ ๋‹ค์Œ ์œค๋…„ ๋ฐ˜ํ™˜ ์—ฌ๋ถ€ ํ™•์ธ ex) expandEventsToNextOccurrences ์œค๋…„ ์ฒดํฌ
describe('expandEventsToNextOccurrences - yearly', () => {
  it('yearly(2/29) ์ด๋ฒคํŠธ๋Š” ๋‹ค์Œ ์œค๋…„ 2/29๋กœ ํ™•์žฅ๋˜๊ณ  id์— ๋‚ ์งœ๊ฐ€ ๊ฒฐํ•ฉ๋œ๋‹ค', () => {
    const events = [
      makeEvent({
        id: 'e3',
        title: 'Yearly',
        date: '2024-02-29',
        repeat: { type: 'yearly', interval: 1 },
      }),
    ];

    const now = new Date('2025-02-28T00:00:00Z');
    const expanded = expandEventsToNextOccurrences(events, now);
    expect(expanded).toHaveLength(1);
    expect(expanded[0].date).toBe('2028-02-29');
    expect(String(expanded[0].id)).toBe('e3:2028-02-29');
  });
});
  • getNextDailyOccurrence, getNextWeeklyOccurrence, getNextYearlyOccurrence

    • ๊ธฐ์ค€์ผ์—์„œ ํ•˜๋ฃจ, 1์ฃผ, 1๋…„ ์ดํ›„์˜ ๋‚ ์งœ ๋ฐ˜ํ™˜ ์—ฌ๋ถ€ ํ™•์ธ
    • ์œค๋…„์˜ ๊ฒฝ์šฐ ๋‹ค์Œ ์œค๋…„ ๋ฐ˜ํ™˜ ์—ฌ๋ถ€ ํ™•์ธ
  • generateInstances

    • ๋ฒ”์œ„ ๋‚ด ๋ฐ˜๋ณต ์ผ์ • ๋ฐ˜ํ™˜ ์—ฌ๋ถ€ ํ™•์ธ
    • ๊ฐ™์€ ์š”์ผ, 31์ผ, ์œค๋…„ ์ฒดํฌ

2. helpers.spec.ts

  • getNextYearlyOccurrence: 2/29 ์‹œ์ž‘, from=ํ‰๋…„ โ†’ ๋‹ค์Œ ์œค๋…„ ex) getNextYearlyOccurrence๊ฐ€ ๋‹ค์Œ ์œค๋…„์„ ์ •ํ™•ํžˆ ๋ฐ˜ํ™˜ํ•˜๋Š”์ง€ ํ™•์ธ
it('getNextYearlyOccurrence: 2/29 ์‹œ์ž‘, from=ํ‰๋…„ โ†’ ๋‹ค์Œ ์œค๋…„', () => {
    const base = dateStringToUtcDateOnly('2024-02-29');
    const from = dateStringToUtcDateOnly('2025-02-28');
    const next = getNextYearlyOccurrence(base, from, 1);
    expect(toDateStringUTC(next)).toBe('2028-02-29');
});
  • getNextMonthlyOccurrence: 1/31 ์‹œ์ž‘, from=2์›” โ†’ 3/31
  • getNextWeeklyOccurrence: ์ˆ˜์š”์ผ ์‹œ์ž‘, from=๋‹ค์Œ์ฃผ ์›”์š”์ผ โ†’ ๋‹ค์Œ ์ˆ˜์š”์ผ
  • getNextDailyOccurrence: from์ด ๋” ๋’ค๋ฉด interval์— ๋งž๊ฒŒ ๋‹ค์Œ ๋‚ ์งœ
  • clampEndDate๋Š” ๋” ์ด๋ฅธ ๋‚ ์งœ๋ฅผ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•œ๋‹ค
  • daysInMonthUTC๋Š” 2์›” ์œค๋…„/ํ‰๋…„๊ณผ 30/31์ผ์„ ์ •ํ™•ํžˆ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•œ๋‹ค
  • addMonthsUntilHasDay๋Š” 1/31์—์„œ 2์›”์„ ๊ฑด๋„ˆ๋›ฐ๊ณ  3/31์„ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•œ๋‹ค

โ–ถ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ

1. ๋ฐ˜๋ณต UI ๋ฐ ํ‘œ์‹œ

  • ๋ฐ˜๋ณต ์ผ์ •์„ ์ €์žฅํ•˜๋ฉด ์บ˜๋ฆฐ๋” ์…€์— ๋ฐ˜๋ณต ์•„์ด์ฝ˜(โ†ป)์ด ํ‘œ์‹œ๋œ๋‹ค ex) ๋ฐ˜๋ณต ์ผ์ • ์ƒ์„ฑ ํ›„ ์บ˜๋ฆฐ๋”์—์„œ ๋ฐ˜๋ณต์ผ์ • ์•„์ด์ฝ˜ ํ™•์ธ
it('๋ฐ˜๋ณต ์ผ์ •์„ ์ €์žฅํ•˜๋ฉด ์บ˜๋ฆฐ๋” ์…€์— ๋ฐ˜๋ณต ์•„์ด์ฝ˜(โ†ป)์ด ํ‘œ์‹œ๋œ๋‹ค', async () => {
    setupMockHandlerCreation();

    vi.setSystemTime(new Date('2025-10-15'));

    const { user } = setup(<App />);

    await user.click(screen.getAllByText('์ผ์ • ์ถ”๊ฐ€')[0]);

    await user.type(screen.getByLabelText('์ œ๋ชฉ'), '๋ฐ˜๋ณต ํšŒ์˜');
    await user.type(screen.getByLabelText('๋‚ ์งœ'), '2025-10-15');
    await user.type(screen.getByLabelText('์‹œ์ž‘ ์‹œ๊ฐ„'), '09:00');
    await user.type(screen.getByLabelText('์ข…๋ฃŒ ์‹œ๊ฐ„'), '10:00');
    await user.type(screen.getByLabelText('์„ค๋ช…'), '๋ฐ˜๋ณต ํ…Œ์ŠคํŠธ');
    await user.type(screen.getByLabelText('์œ„์น˜'), 'ํšŒ์˜์‹ค C');

    await user.click(screen.getByLabelText('์นดํ…Œ๊ณ ๋ฆฌ'));
    await user.click(within(screen.getByLabelText('์นดํ…Œ๊ณ ๋ฆฌ')).getByRole('combobox'));
    await user.click(screen.getByRole('option', { name: '์—…๋ฌด-option' }));

    const repeatToggle = await screen.findByLabelText('๋ฐ˜๋ณต ์ผ์ •');
    await user.click(repeatToggle);
    await user.click(await screen.findByRole('combobox', { name: '๋ฐ˜๋ณต ์œ ํ˜•' }));
    await user.click(screen.getByRole('option', { name: '๋งค์ฃผ' }));

    await user.click(screen.getByTestId('event-submit-button'));

    await user.click(within(screen.getByLabelText('๋ทฐ ํƒ€์ž… ์„ ํƒ')).getByRole('combobox'));
    await user.click(screen.getByRole('option', { name: 'week-option' }));
    const weekView = within(screen.getByTestId('week-view'));
    expect(weekView.getAllByLabelText('๋ฐ˜๋ณต ์ผ์ • ์•„์ด์ฝ˜').length).toBeGreaterThan(0);
});
  • ๋ฐ˜๋ณต ์ผ์ • ์ฒดํฌ ์‹œ ๋ฐ˜๋ณต ์œ ํ˜•/์ข…๋ฃŒ์ผ ์ž…๋ ฅ UI๊ฐ€ ๋…ธ์ถœ๋œ๋‹ค
  • ๋ฐ˜๋ณต ์ผ์ •์„ ๋‹จ์ผ ์ˆ˜์ •์œผ๋กœ ์ €์žฅํ•˜๋ฉด ๋ฐ˜๋ณต ์•„์ด์ฝ˜์ด ์‚ฌ๋ผ์ง„๋‹ค
  • ๋ฐ˜๋ณต์ผ์ •์„ ๋‹จ์ผ ์‚ญ์ œํ•˜๋ฉด ํ•ด๋‹น ๋‚ ์งœ ์…€์—์„œ๋งŒ ์‚ฌ๋ผ์ง„๋‹ค

โ–ถ e2e ํ…Œ์ŠคํŠธ

1. ๊ธฐ๋ณธ + ๋ฐ˜๋ณต ์•„์ด์ฝ˜

  • ์•ฑ์ด ๋กœ๋“œ๋˜๊ณ  ๊ธฐ๋ณธ UI๊ฐ€ ๋ณด์ธ๋‹ค
  • ๋ฐ˜๋ณต ์ผ์ • ์ €์žฅ ์‹œ ์บ˜๋ฆฐ๋” ์…€์— ์•„์ด์ฝ˜์ด ๋ณด์ธ๋‹ค

2. ๋‹จ์ผ ์ผ์ • ์‚ญ์ œ

  • ๋‹จ์ผ ์ผ์ • ์‚ญ์ œ๊ฐ€ ์„ฑ๊ณตํ•˜๊ณ  ํ™”๋ฉด์—์„œ ์‚ฌ๋ผ์ง„๋‹ค

ex) ์ผ์ • ์ถ”๊ฐ€ ๋ฒ„ํŠผ -> ์ž…๋ ฅ -> ์ผ์ • ๊ฒน์นจ ๊ฒฝ๊ณ  -> ๋ฐ˜๋ณต ์ผ์ • ๋“ฑ๋ก ์™„๋ฃŒ -> ์ผ์ • ์‚ญ์ œ -> ๋‹จ์ผ ์ผ์ • ์‚ญ์ œ ํ™•์ธ

test.describe('E2E - ๋‹จ์ผ ์ผ์ • ์‚ญ์ œ', () => {
  test('๋‹จ์ผ ์ผ์ • ์‚ญ์ œ๊ฐ€ ์„ฑ๊ณตํ•˜๊ณ  ํ™”๋ฉด์—์„œ ์‚ฌ๋ผ์ง„๋‹ค', async ({ page }) => {
    await page.goto('/');

    const todayStr = (() => {
      const now = new Date();
      const yyyy = now.getFullYear();
      const mm = String(now.getMonth() + 1).padStart(2, '0');
      const dd = String(now.getDate()).padStart(2, '0');
      return `${yyyy}-${mm}-${dd}`;
    })();

    await page.locator('text=์ผ์ • ์ถ”๊ฐ€').first().click();
    const uniqueTitle = `์‚ญ์ œ ํ…Œ์ŠคํŠธ ${Math.random().toString(36).slice(2, 7)}`;
    await page.fill('input[id="title"]', uniqueTitle);
    await page.fill('input[id="date"]', todayStr);
    await page.fill('input[id="start-time"]', '09:00');
    await page.fill('input[id="end-time"]', '10:00');
    await page.locator('[aria-labelledby="category-label"]').click();
    await page
      .getByRole('option', { name: /๊ฐœ์ธ|์—…๋ฌด/ })
      .first()
      .click();
    await page.getByTestId('event-submit-button').click();

    const overlapDialog = page.getByRole('dialog', { name: '์ผ์ • ๊ฒน์นจ ๊ฒฝ๊ณ ' });
    if (await overlapDialog.isVisible().catch(() => false)) {
      await overlapDialog.getByRole('button', { name: '๊ณ„์† ์ง„ํ–‰' }).click();
    }
    await expect(page.getByText('์ผ์ •์ด ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.')).toBeVisible();

    const beforeCount = await page.locator('button[aria-label^="Delete event"]').count();
    const beforeTitleCount = await page.getByText(uniqueTitle).count();

    const titleNode = page.getByTestId('event-list').getByText(uniqueTitle).first();
    const deleteBtn = titleNode
      .locator('xpath=ancestor::*[contains(@class, "MuiBox-root")][1]')
      .locator('xpath=.//button[starts-with(@aria-label, "Delete event")]');
    await deleteBtn.first().click();

    await expect(page.getByText('์ผ์ •์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.')).toBeVisible();
    const afterTitleCount = await page.getByText(uniqueTitle).count();
    expect(afterTitleCount).toBeLessThan(beforeTitleCount);

    const afterCount = await page.locator('button[aria-label^="Delete event"]').count();
    expect(afterCount).toBe(beforeCount - 1);

    await expect(page.getByLabel('๋ฐ˜๋ณต ์ผ์ • ์•„์ด์ฝ˜').first()).toBeVisible();
  });
});

๊ณผ์ œ ์…€ํ”„ํšŒ๊ณ 

์ด๋ฒˆ ํ…Œ์ŠคํŠธ์ฝ”๋“œ 7, 8์ฃผ์ฐจ ๋˜๊ฒŒ ์žฌ๋ฐŒ์—ˆ์Šต๋‹ˆ๋‹ค. ํ•˜๋‚˜์”ฉ ํผ์ฆ์„ ๋งž์ถฐ๊ฐ€๋Š” ๋А๋‚Œ์ด๋ž„๊นŒ์š”. ๋‹ค๋งŒ TDD๋Š” ์ƒ๊ฐ๋ณด๋‹ค ์–ด๋ ค์› ์Šต๋‹ˆ๋‹ค. ์ฒ˜์Œ ์„ค๋ช…์„ ๋“ค์—ˆ์„ ๋•Œ๋Š” ๋˜๊ฒŒ ๊ฐ„๋‹จํ•˜๊ฒŒ ๋А๊ปด์กŒ์Šต๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ ์ฝ”๋“œ(RED) ๋จผ์ € ์ž‘์„ฑ ํ›„ ์ตœ์†Œํ•œ์˜ ๊ตฌํ˜„(GREEN), ์ดํ›„ ๋ฆฌํŒฉํ† ๋ง(Refactor). ํ•˜๋‚˜๋„ ์–ด๋ ค์šธ ๊ฒŒ ์—†์–ด ๋ณด์˜€์Šต๋‹ˆ๋‹ค...๋งŒ

์šฐ์„  ์ตœ์†Œํ•œ์˜ ๊ตฌํ˜„์ด ์ƒ๊ฐ๋ณด๋‹ค ์‰ฝ์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. RED๋ฅผ 1๋งŒํผ ์ž‘์„ฑํ•œ ํ›„ 1๋งŒํผ์˜ GREEN์„ ๊ตฌํ˜„ํ•˜๋ ค ํ–ˆ์ง€๋งŒ ์ด๋ฏธ ์š”๊ตฌ์‚ฌํ•ญ์„ ์•Œ๊ณ  ์žˆ๊ธฐ์— ์ž๊พธ ๋” ๋‚˜์•„๊ฐ€ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋ฌผ๋ก  ์š”๊ตฌ์‚ฌํ•ญ์„ ์ธ์ง€ํ•˜๊ณ  ์ถ”๊ฐ€์ ์ธ ๋ฐฉํ–ฅ๊นŒ์ง€ ๊ณ ๋ คํ•˜๋ฉฐ ๊ฐœ๋ฐœํ•˜๋Š” ๊ฒƒ์ด ๋‚˜์œ ๊ฒƒ์€ ์•„๋‹ˆ์ง€๋งŒ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ณธ์งˆ์„ ๋†“์น˜๊ฒŒ ๋œ๋‹ค.

  • ๊ณผ๋„ํ•œ ๋ณต์žก์„ฑ ์กฐ๊ธฐ ๋„์ž…: ์•„์ง ํ•„์š”ํ•˜์ง€ ์•Š์€ ์ถ”์ƒํ™”๋‚˜ ์ผ๋ฐ˜ํ™”๋ฅผ ๋ฏธ๋ฆฌ ๋งŒ๋“ค์–ด๋ฒ„๋ฆฐ๋‹ค.
  • ๋””๋ฒ„๊น… ๋ฒ”์œ„ ํ™•๋Œ€: ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํŒจํ–ˆ์„ ๋•Œ ๋ฌธ์ œ์˜ ์›์ธ์ด ๋  ์ˆ˜ ์žˆ๋Š” ์ฝ”๋“œ ๋ฒ”์œ„๊ฐ€ ๋„“์–ด์ง„๋‹ค.
  • ๋ฆฌํŒฉํ† ๋ง ํƒ€์ด๋ฐ ๋†“์นจ: Green ๋‹จ๊ณ„์—์„œ ์ด๋ฏธ "๊น”๋”ํ•œ" ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ–ˆ๋‹ค๊ณ  ์ƒ๊ฐํ•˜๋ฉด Refactor ๋‹จ๊ณ„๋ฅผ ๊ฑด๋„ˆ๋›ฐ๊ธฐ ์‰ฝ๋‹ค.
  • ํ…Œ์ŠคํŠธ์™€ ๊ตฌํ˜„์˜ ๊ฒฐํ•ฉ๋„ ์ฆ๊ฐ€: ํ•œ ๋ฒˆ์— ๋งŽ์€ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๋ฉด ํ…Œ์ŠคํŠธ๊ฐ€ ๊ตฌํ˜„ ์„ธ๋ถ€์‚ฌํ•ญ์— ๋” ๋งŽ์ด ์˜์กดํ•˜๊ฒŒ ๋˜์–ด ๋‚˜์ค‘์— ๋ฆฌํŒฉํ† ๋ง์„ ์–ด๋ ต๊ฒŒ ๋งŒ๋“ ๋‹ค.
  • ์ธ์ง€ ๋ถ€ํ•˜ ์ฆ๊ฐ€: ๋™์‹œ์— ๊ณ ๋ คํ•ด์•ผ ํ•  ๊ฒƒ๋“ค์ด ๋งŽ์•„์ ธ์„œ ์‹ค์ˆ˜ํ•  ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์•„์ง€๊ณ , ๊ฐ ๊ฒฐ์ •์˜ ์˜ํ–ฅ์„ ์ •ํ™•ํžˆ ํŒŒ์•…ํ•˜๊ธฐ ์–ด๋ ค์›Œ์ง„๋‹ค.
  • ํ•™์Šต ๊ธฐํšŒ ์ƒ์‹ค: TDD์˜ ์ง„์งœ ๊ฐ€์น˜๋Š” ์ž‘์€ ๋‹จ๊ณ„๋ฅผ ํ†ตํ•ด ์–ป๋Š” ํ†ต์ฐฐ๋ ฅ์ธ๋ฐ, ์ด๋ฅผ ๊ฑด๋„ˆ๋›ฐ๋ฉด์„œ TDD์˜ ์ง„์งœ ์žฅ์ ์„ ์ฒดํ—˜ํ•  ๊ธฐํšŒ๋ฅผ ๋†“์น˜๊ฒŒ ๋œ๋‹ค.

TDD๋ฅผ ์„ฑ์‹คํžˆ ์ดํ–‰ํ•˜๊ธฐ ์œ„ํ•ด์„  ๋งŽ์€ ์—ฐ์Šต์ด ํ•„์š”ํ•  ๊ฒƒ ๊ฐ™๋‹ค. ๊ธฐ์กด์— ๊ฐœ๋ฐœํ•˜๋˜ ๊ด€์„ฑ์„ ๋ฒ„๋ฆฌ๊ณ  ์˜์‹์ ์œผ๋กœ TDD ๊ทผ์œก์„ ํ‚ค์šธ ํ•„์š”๊ฐ€ ์žˆ์ง€ ์•Š์„๊นŒ ์ƒ๊ฐํ•œ๋‹ค.

๊ธฐ์ˆ ์  ์„ฑ์žฅ

์šฐ์„  TDD๋ฅผ ์ฒ˜์Œ์œผ๋กœ ๊ฒฝํ—˜ํ•ด ๋ณด์•˜๋‹ค. ๊ทธ๊ฐ„ ๋ง๋กœ๋งŒ ๋“ฃ๋˜ TDD, ์—ญ์‹œ ํ•œ ๋ฒˆ์ด๋ผ๋„ ํ•ด ๋ณด๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•˜๋‹ค. ๋” ์ด์ƒ TDD๋ผ๋Š” ๋ง์ด ์—„์ฒญ ํฌ๊ฒŒ ๋А๊ปด์ง€๊ฑฐ๋‚˜ ๋‚ฏ์„ค์ง€๋Š” ์•Š๋‹ค. ๋งŒ์•ฝ ๋‹ค์Œ ํšŒ์‚ฌ์—์„œ TDD๋กœ ๊ฐœ๋ฐœํ•œ๋‹ค๊ณ  ํ•˜๋”๋ผ๋„ ํฐ ๋‘๋ ค์›€ ์—†์ด ํ•  ์ˆ˜ ์žˆ์ง€ ์•Š์„๊นŒ

๋‘ ๋ฒˆ์งธ๋กœ 2์ฃผ ๊ฐ„์˜ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ฃผ์ฐจ๋ฅผ ๊ฑฐ์น˜๋ฉฐ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์— ์ž์‹ ๊ฐ์ด ์ƒ๊ฒผ๋‹ค. ์–ด๋–ค ๋‹จ์œ„๋กœ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ๋‚˜๋ˆ„์–ด์•ผ ํ• ์ง€, ์–ด๋–ค ๊ธฐ๋Šฅ์„ ์ค‘์ ์ ์œผ๋กœ ํ…Œ์ŠคํŠธํ•ด์•ผ ํ• ์ง€, ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋Š” ์–ด๋–ป๊ฒŒ ์ž‘์„ฑํ•˜๋Š”์ง€ ์•Œ๊ฒŒ ๋˜์—ˆ๋‹ค.

์ฝ”๋“œ ํ’ˆ์งˆ

const saveEvent = async (eventData: Event | EventForm) => {
    try {
      if (editing) {
        if (eventData.repeat.type !== 'none') {
          const copyData = { ...eventData } as Event;
          const repeatEvents = generateRepeatEvents(copyData);

          await deleteEvent(copyData.id);
          await addEvents([copyData, ...repeatEvents]);
        } else {
          // ๋ฐ˜๋ณต ์„ค์ •์ด ์—†์œผ๋ฉด repeat ์ดˆ๊ธฐํ™”ํ•ด์„œ ์ˆ˜์ •
          await updateEvent({ ...eventData, repeat: { type: 'none', interval: 0 } } as Event);
        }
      } else {
        if (eventData.repeat.type !== 'none') {
          // ๊ธฐ์ค€ ์ผ์ • + ์ถ”๊ฐ€ ๋ฐ˜๋ณต ์ผ์ •๋“ค์„ ํ•œ ๋ฒˆ์— ์ƒ์„ฑ
          const repeatEvents = generateRepeatEvents(eventData);
          const allEvents = [eventData, ...repeatEvents];

          await addEvents(allEvents);
        } else {
          await addEvent(eventData);
        }
      }

      await fetchEvents();
      onSave?.();
      enqueueSnackbar(editing ? '์ผ์ •์ด ์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค.' : '์ผ์ •์ด ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.', {
        variant: 'success',
      });
    } catch (error) {
      console.error('Error saving event:', error);
      enqueueSnackbar('์ผ์ • ์ €์žฅ ์‹คํŒจ', { variant: 'error' });
    }
};

์ด๋ฒˆ์— ๊ธ‰ํ•˜๊ฒŒ ์ง ๋‹ค๊ณ  saveEvent๋ฅผ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•˜๊ณ  ์ด ์•ˆ์—์„œ ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ ํ•˜์˜€์ง€๋งŒ saveEvent์˜ ์—ญํ• ์ด ๋„ˆ๋ฌด ๋ฌด๊ฒ๋‹ค๊ณ  ์ƒ๊ฐํ•œ๋‹ค.

๋ฆฌ๋ทฐ ๋ฐ›๊ณ  ์‹ถ์€ ๋‚ด์šฉ

์ด๋ฒˆ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ๋น„๋กฏํ•˜์—ฌ ํ•ญํ•ด์—์„œ ๋ฆฌ์•กํŠธ๋„ ์ง์ ‘ ๋งŒ๋“ค์–ด ๋ณด๊ณ  ํด๋ฆฐ ์ฝ”๋“œ๋‚˜ fsd๋„ ๋ฐฐ์› ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด์ „ ํšŒ์‚ฌ์—์„œ ์‚ฌ์šฉํ•ด ๋ณด์ง„ ์•Š์•˜๊ธฐ์— ์ด์ง ์‹œ ์ด๋ฅผ ์–ด๋–ป๊ฒŒ ์–ดํ•„ํ•  ์ˆ˜ ์žˆ์„์ง€ ๊ถ๊ธˆํ•ฉ๋‹ˆ๋‹ค!

๊ณผ์ œ ํ”ผ๋“œ๋ฐฑ

ํ™์ค€๋‹˜ ์ˆ˜๊ณ ํ•˜์…จ์Šต๋‹ˆ๋‹ค!

Q. ์ด๋ฒˆ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ๋น„๋กฏํ•˜์—ฌ ํ•ญํ•ด์—์„œ ๋ฆฌ์•กํŠธ๋„ ์ง์ ‘ ๋งŒ๋“ค์–ด ๋ณด๊ณ  ํด๋ฆฐ ์ฝ”๋“œ๋‚˜ fsd๋„ ๋ฐฐ์› ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด์ „ ํšŒ์‚ฌ์—์„œ ์‚ฌ์šฉํ•ด ๋ณด์ง„ ์•Š์•˜๊ธฐ์— ์ด์ง ์‹œ ์ด๋ฅผ ์–ด๋–ป๊ฒŒ ์–ดํ•„ํ•  ์ˆ˜ ์žˆ์„์ง€ ๊ถ๊ธˆํ•ฉ๋‹ˆ๋‹ค!

A. ์–ดํ•„ํ• ์ˆ˜์žˆ๋Š” ๊ฐ€์žฅ ์ข‹์€ ๋ฐฉ๋ฒ•์€ ํ”„๋กœ์ ํŠธ๋‚˜ ์•„๋ฌดํŠผ ์—…๋ฌด์— ๋„์ž…์„ ํ•ด์•ผํ•  ๊ฒƒ ๊ฐ™์•„์š”.

๊ทธ๋Ÿด์ˆ˜ ์žˆ๋Š” ์ƒํ™ฉ์ด ์•„๋‹Œ ์ง€๊ธˆ์—๋Š” ๋‘๊ฐ€์ง€ ์ •๋„ ์ƒ๊ฐํ•ด๋ณผ ์ˆ˜ ์žˆ๋Š”๋ฐ์š”. ๊ณผ์ œ์— ์ ์šฉํ•˜๋Š” ๋งŒํผ์€ ํšจ๊ณผ๊ฐ€ ์—†์„ ๊ฒƒ ๊ฐ™์•„์š”

์ฒซ๋ฒˆ์งธ๋Š” ํ•™์Šตํ•˜๋ฉด์„œ ์–ป์€ ํ†ต์ฐฐ๋กœ ๋ธ”๋กœ๊ทธ ๊ธ€์„ ์ž‘์„ฑํ•ด์„œ ์–ดํ•„ํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ ๊ฐ™๊ณ ์š”.

๋‘๋ฒˆ์งธ๋Š” ์‚ฌ์ด๋“œ ํ”„๋กœ์ ํŠธ์—์„œ ์ ์šฉํ•ด๋ณด๋Š” ๊ฒƒ์ผ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. ์‚ฌ์ด๋“œ ํ”„๋กœ์ ํŠธ ์„ค๋ช…์— ๋ง์”€ํ•˜์‹  ๊ธฐ์ˆ ์ ์ธ ์„ฑ๊ณผ๋ฅผ ์ ์–ด์„œ ๋ง์ด์ฃต

์ˆ˜๊ณ ํ•˜์…จ์Šต๋‹ˆ๋‹ค ํ™์ค€๋‹˜! ๊ทธ๋ฆฌ๊ณ  ํ™”์ดํŒ…!!! ๋ถ„๋ช… ์ข‹์€ ํšŒ์‚ฌ๋กœ ์ž…์‚ฌํ•˜์‹ค ์ˆ˜ ์žˆ์œผ์‹ค๊ฑฐ์—์š”!