8์ฃผ์ฐจ ๊ณผ์ ์ฒดํฌํฌ์ธํธ
๊ธฐ๋ณธ ๊ณผ์
ํ์
- ๋ฐ๋ณต ์ ํ ์ ํ
- ์ผ์ ์์ฑ ๋๋ ์์ ์ ๋ฐ๋ณต ์ ํ์ ์ ํํ ์ ์๋ค.
- ๋ฐ๋ณต ์ ํ์ ๋ค์๊ณผ ๊ฐ๋ค: ๋งค์ผ, ๋งค์ฃผ, ๋งค์, ๋งค๋
- 31์ผ์ ๋งค์์ ์ ํํ๋ค๋ฉด -> ๋งค์ ๋ง์ง๋ง์ด ์๋, 31์ผ์๋ง ์์ฑํ์ธ์.
- ์ค๋ 29์ผ์ ๋งค๋ ์ ์ ํํ๋ค๋ฉด -> 29์ผ์๋ง ์์ฑํ์ธ์!
- ๋ฐ๋ณต ์ผ์ ํ์
- ์บ๋ฆฐ๋ ๋ทฐ์์ ๋ฐ๋ณต ์ผ์ ์ ์๊ฐ์ ์ผ๋ก ๊ตฌ๋ถํ์ฌ ํ์ํ๋ค.
- ์์ด์ฝ์ ๋ฃ๋ ํ๊ทธ๋ฅผ ๋ฃ๋ ์์ ๋กญ๊ฒ ํด๋ณด์ธ์!
- ์บ๋ฆฐ๋ ๋ทฐ์์ ๋ฐ๋ณต ์ผ์ ์ ์๊ฐ์ ์ผ๋ก ๊ตฌ๋ถํ์ฌ ํ์ํ๋ค.
- ๋ฐ๋ณต ์ข
๋ฃ
- ๋ฐ๋ณต ์ข ๋ฃ ์กฐ๊ฑด์ ์ง์ ํ ์ ์๋ค.
- ์ต์ : ํน์ ๋ ์ง๊น์ง, ํน์ ํ์๋งํผ, ๋๋ ์ข ๋ฃ ์์ (์์ ํน์ฑ์, 2025-06-30๊น์ง)
- ๋ฐ๋ณต ์ผ์ ๋จ์ผ ์์
- ๋ฐ๋ณต์ผ์ ์ ์์ ํ๋ฉด ๋จ์ผ ์ผ์ ์ผ๋ก ๋ณ๊ฒฝ๋ฉ๋๋ค.
- ๋ฐ๋ณต์ผ์ ์์ด์ฝ๋ ์ฌ๋ผ์ง๋๋ค.
- ๋ฐ๋ณต ์ผ์ ๋จ์ผ ์ญ์
- ๋ฐ๋ณต์ผ์ ์ ์ญ์ ํ๋ฉด ํด๋น ์ผ์ ๋ง ์ญ์ ํฉ๋๋ค.
์ ํ
- ๋ฐ๋ณต ๊ฐ๊ฒฉ ์ค์
- ๊ฐ ๋ฐ๋ณต ์ ํ์ ๋ํด ๊ฐ๊ฒฉ์ ์ค์ ํ ์ ์๋ค.
- ์: 2์ผ๋ง๋ค, 3์ฃผ๋ง๋ค, 2๊ฐ์๋ง๋ค ๋ฑ
- ์์ธ ๋ ์ง ์ฒ๋ฆฌ:
- ๋ฐ๋ณต ์ผ์ ์ค ํน์ ๋ ์ง๋ฅผ ์ ์ธํ ์ ์๋ค.
- ๋ฐ๋ณต ์ผ์ ์ค ํน์ ๋ ์ง์ ์ผ์ ์ ์์ ํ ์ ์๋ค.
- ์์ผ ์ง์ (์ฃผ๊ฐ ๋ฐ๋ณต์ ๊ฒฝ์ฐ):
- ์ฃผ๊ฐ ๋ฐ๋ณต ์ ํน์ ์์ผ์ ์ ํํ ์ ์๋ค.
- ์๊ฐ ๋ฐ๋ณต ์ต์
:
- ๋งค์ ํน์ ๋ ์ง์ ๋ฐ๋ณต๋๋๋ก ์ค์ ํ ์ ์๋ค.
- ๋งค์ ํน์ ์์์ ์์ผ์ ๋ฐ๋ณต๋๋๋ก ์ค์ ํ ์ ์๋ค.
- ๋ฐ๋ณต ์ผ์ ์ ์ฒด ์์ ๋ฐ ์ญ์
- ๋ฐ๋ณต ์ผ์ ์ ๋ชจ๋ ์ผ์ ์ ์์ ํ ์ ์๋ค.
- ๋ฐ๋ณต ์ผ์ ์ ๋ชจ๋ ์ผ์ ์ ์ญ์ ํ ์ ์๋ค.
์ฌํ ๊ณผ์
- ์ด ์ฑ์ ์ ํฉํ ํ ์คํธ ์ ๋ต์ ๋ง๋ค์๋์?
๊ฐ ํ์๋ค์ ํ ์คํธ ์ ๋ต์?
์ ํฌํ์ ํ์๋ณ๋ก ํ ์คํธ ์ ๋ต์ ๋ํ ์๊ฒฌ์ ์ฃผ๊ณ ๋ฐ๊ธฐ ๋ณด๋ค๋ ํผ๊ทธ์ผ์ ํตํด ํจ๊ป ๋ธ๋ ์ธ์คํ ๋ฐํ๋ฉฐ ์ ๋ต์ ์ขํ ๋๊ฐ์ต๋๋ค.
- [9ํ ํ ์คํธ ์ ๋ต ํค๋ ธํธ](https://www.figma.com/board/dYGP9N0xBufQX0NiHFq0Rg/9%ED%8C%80?node-id=0-1&p=f&t=2jT1n4jmI1VUH04 Q-0)
๋ฐ๋ผ์, ํ๋์ PR์ ํ ์คํธ ์ ๋ต์ ์๋ฆฝํ์ฌ ๋ชจ๋ ํ์ด์ฝ๋ฉ์ ์งํํ์ต๋๋ค. ๊ฒฐ๋ก ์ ์ผ๋ก 9ํ์ ์ฌํ๊ณผ์ PR๋ถ๋ถ๊ณผ ์ฝ๋ ์ ๋ต์ ํจ๊ป ์์ฑํ์ฌ ๊ทธ ๋ด์ฉ์ด ๊ฐ์ต๋๋ค.
- ํ ์คํธ ์ ๋ต์ ์ ์ฉํ ์ต์ข ์ฝ๋ 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/31getNextWeeklyOccurrence: ์์์ผ ์์, 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. ์ดํํ ์์๋ ๊ฐ์ฅ ์ข์ ๋ฐฉ๋ฒ์ ํ๋ก์ ํธ๋ ์๋ฌดํผ ์ ๋ฌด์ ๋์ ์ ํด์ผํ ๊ฒ ๊ฐ์์.
๊ทธ๋ด์ ์๋ ์ํฉ์ด ์๋ ์ง๊ธ์๋ ๋๊ฐ์ง ์ ๋ ์๊ฐํด๋ณผ ์ ์๋๋ฐ์. ๊ณผ์ ์ ์ ์ฉํ๋ ๋งํผ์ ํจ๊ณผ๊ฐ ์์ ๊ฒ ๊ฐ์์
์ฒซ๋ฒ์งธ๋ ํ์ตํ๋ฉด์ ์ป์ ํต์ฐฐ๋ก ๋ธ๋ก๊ทธ ๊ธ์ ์์ฑํด์ ์ดํํ ์ ์์ ๊ฒ ๊ฐ๊ณ ์.
๋๋ฒ์งธ๋ ์ฌ์ด๋ ํ๋ก์ ํธ์์ ์ ์ฉํด๋ณด๋ ๊ฒ์ผ ๊ฒ ๊ฐ์ต๋๋ค. ์ฌ์ด๋ ํ๋ก์ ํธ ์ค๋ช ์ ๋ง์ํ์ ๊ธฐ์ ์ ์ธ ์ฑ๊ณผ๋ฅผ ์ ์ด์ ๋ง์ด์ฃต
์๊ณ ํ์ จ์ต๋๋ค ํ์ค๋! ๊ทธ๋ฆฌ๊ณ ํ์ดํ !!! ๋ถ๋ช ์ข์ ํ์ฌ๋ก ์ ์ฌํ์ค ์ ์์ผ์ค๊ฑฐ์์!