๐Ÿจ ๋ฒ ์ŠคํŠธ๋ผ๋นˆ์Šค ํ”„๋กœ์ ํŠธ

์•ฝ 10์ผ๋™์•ˆ ๊ธฐํš๋ถ€ํ„ฐ ๋””์ž์ธ, ๊ฐœ๋ฐœ๊นŒ์ง€ ์ง„ํ–‰ํ•œ ๋ฒ ์ŠคํŠธ๋ผ๋นˆ์Šค ํ”„๋กœ์ ํŠธ. ๋™๋ฃŒ์—๊ฒŒ ์ถ”์ฒœ๋ฐ›์•„ ์•Œ๊ฒŒ๋œ ํฌํ…๋ฐ์ด๋ฅผ ํ†ตํ•ด ์ง€์›ํ–ˆ๊ณ  ์ข‹์€ ๊ธฐํš์ž๋ถ„๊ณผ ๋””์ž์ด๋„ˆ๋ถ„์„ ๋งŒ๋‚˜์„œ ์—ด์‹ฌํžˆ ๋‹ฌ๋ ค๋ณผ ์ˆ˜ ์žˆ์—ˆ๋‹ค. ์‚ฌ์‹ค ๋งค์ผ๋งค์ผ ํšŒ๊ณ ๋ฅผ ์ž‘์„ฑํ•˜๋ คํ–ˆ๋Š”๋ฐ, 10์ผ์ด๋ผ๋Š” ์งง์€ ์‹œ๊ฐ„๋™์•ˆ ํ•˜๋‚˜์˜ ์„œ๋น„์Šค๋ฅผ MVP๊นŒ์ง€ ์™„์„ฑํ•˜๋Š” ๊ฒŒ ์‰ฌ์šด ์ผ์ด ์•„๋‹ˆ์—ˆ๋‹ค. ํ•ด์„œ ์šฐ๋ฆฌ ํŒ€์ด 10์ผ ๋™์•ˆ ์–ด๋–ค ๊ณผ์ •์„ ๊ฑฐ์ณ์„œ ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ–ˆ๋Š”์ง€, ๊ทธ๋ฆฌ๊ณ  ๋‚˜๋Š” ์–ด๋–ค ์—ญํ• ์„ ๋งก์•„์„œ ์–ด๋–ค ๊ณต๋ถ€๋ฅผ ํ–ˆ๋Š”์ง€์— ๋Œ€ํ•ด ํšŒ๊ณ ํ•ด๋ณด๋ ค๊ณ  ํ•œ๋‹ค.(1์ฐจ ํšŒ๊ณ ์ธ ์ด์œ ๋Š” ์ดํ›„์—๋„ ๋ช‡๋ช‡ ๊ธฐ๋Šฅ์„ ๋”ํ•˜๊ณ  ๋ฆฌํŒฉํ† ๋ง์„ ์ง„ํ–‰ํ•  ์˜ˆ์ •์ด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค!)

๋ฒ ์ŠคํŠธ ๋ผ๋นˆ์Šค: ๋ฐฐํฌ ๋งํฌ

1. ์•„์ด๋””์–ด ํšŒ์˜ ๋ฐ ํŒ€ ๋ชฉํ‘œ ์„ค์ •

๊ฐ์ž ์–ด๋–ค ์„œ๋น„์Šค๋ฅผ ํ•˜๊ณ  ์‹ถ์€์ง€ ์ƒ๊ฐํ•ด์˜ค๊ธฐ๋กœ ํ–ˆ๋‹ค. ์—ฌ๋Ÿฌ๊ฐ€์ง€ ์•„์ด๋””์–ด๋“ค ์ค‘ ์šฐ์„  10์ผ ์•ˆ์— MVP ๊ธฐํš ๋ฐ ๋””์ž์ธ, ๊ฐœ๋ฐœ์ด ๊ฐ€๋Šฅํ•˜๊ณ  ์ถ”ํ›„ ํ™•์žฅ์ด ๊ฐ€๋Šฅํ•œ ์•„์ดํ…œ์„ ๊ณ ๋ฅด๊ธฐ๋กœ ํ–ˆ๊ณ  ๊ทธ ๊ฒฐ๊ณผ ์‚ฌ๋žŒ๋“ค์—๊ฒŒ ๋ฐฐ์Šคํ‚จ ๋ผ๋นˆ์Šค ์•„์ด์Šคํฌ๋ฆผ ์กฐํ•ฉ์„ ์ถ”์ฒœํ•ด์ฃผ๋Š” ์„œ๋น„์Šค๋ฅผ ๊ฐœ๋ฐœํ•˜๊ธฐ๋กœ ํ–ˆ๋‹ค. ๊ตฌ์ฒด์ ์ธ ๊ธฐํš๊ณผ ๋””์ž์ธ, ์ถ”์ฒœ ๋ฐฉ๋ฒ•์€ ์ •ํ•ด์ง€์ง€ ์•Š์•˜์ง€๋งŒ, ์šฐ์„ ์ ์œผ๋กœ 10์ผ ์•ˆ์— MVP๋ฅผ ์™„์„ฑํ•˜๋Š” ๊ฒƒ์„ ์ตœ์šฐ์„ ์œผ๋กœ ํ•˜๊ธฐ๋กœ ํ–ˆ๋‹ค.

์ฒซ ํšŒ์˜

2. ๊ธฐํš ๋ฐ ๋””์ž์ธ

๊ฐ์ž ๋ณธ์ธ์ด ์ƒ๊ฐํ•˜๋Š” ์šฐ๋ฆฌ ์„œ๋น„์Šค์˜ ๊ธฐํš๊ณผ ๋ชจ์Šต์„ ์ƒ๊ฐํ•˜๊ณ  ๊ฐ€์ ธ์˜ค๊ธฐ๋กœ ํ–ˆ๋‹ค! ๋‚˜ ๊ฐ™์€ ๊ฒฝ์šฐ, ์•„์ด์Šคํฌ๋ฆผ ๋ชฉ๋ก์„ ๋ณด์—ฌ์ฃผ๊ณ  ์œ ์ €๊ฐ€ ์„ ํƒ์„ ํ•˜๋ฉด ํ•ด๋‹น ์•„์ด์Šคํฌ๋ฆผ๋“ค์˜ ์กฐํ•ฉ์˜ ๋Šฅ๋ ฅ์น˜(?)๋ฅผ ๋ณด์—ฌ์คŒ์œผ๋กœ์จ ํ•ด๋‹น ์กฐํ•ฉ์„ ์ถ”์ฒœํ•˜๋Š”์ง€ ๊ทธ๋ ‡์ง€ ์•Š์€์ง€๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ์„œ๋น„์Šค๋ฅผ ์ƒ๊ฐํ–ˆ๋‹ค. ํ•˜์ง€๋งŒ ๊ทธ๊ฒƒ๋ณด๋‹ค๋Š” ๋ฐฐ์Šคํ‚จ ๋ผ๋นˆ์Šค๋ฅผ ๋ชฐ๋ผ๋„ ์„œ๋น„์Šค๋ฅผ ์ด์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ์žฌ๋ฃŒ๋ฅผ ์„ ํƒํ•˜๋ฉด ํ•ด๋‹น ์žฌ๋ฃŒ๊ฐ€ ํฌํ•จ๋œ ์•„์ด์Šคํฌ๋ฆผ ์กฐํ•ฉ์„ ์ถ”์ฒœํ•ด์ฃผ์ž๋Š” ์˜๊ฒฌ์ด ๋‚˜์™”๊ณ  ๊ทธ๋ ‡๊ฒŒ ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค๊ธฐ๋กœ ํ–ˆ๋‹ค. ์•„๋ž˜๋Š” ๋‚ด ๋‚˜๋ฆ„ ์—ด์‹ฌํžˆ ๋งŒ๋“  ์™€์ด์–ดํ”„๋ ˆ์ž„์ด๋‹ค!

์™€์ด์–ด ํ”„๋ ˆ์ž„

๊ทธ๋ฆฌ๊ณ  ์•„๋ž˜๋Š” ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž ์ž…์žฅ์—์„œ ์™€์ด์–ด ํ”„๋ ˆ์ž„์„ ๋”ฐ๋ผ ์ž‘์„ฑํ•ด๋ณธ ์œ ์ € ํ”Œ๋กœ์šฐ์ด๋‹ค.

์œ ์ € ํ”Œ๋กœ์šฐ

3. ๊ฐœ๋ฐœ

๊ฐœ๋ฐœ ํฌ์ธํŠธ

์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ ๋””์ž์ด๋„ˆ๋ถ„๊ป˜์„œ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ž‘์—…ํ•ด์ฃผ์‹œ๋Š” ํ”ผ๊ทธ๋งˆ๋ฅผ ๋ณด๋ฉด์„œ ๊ฐœ๋ฐœ์— ๋“ค์–ด๊ฐ”๋‹ค! ์ด๋ฒˆ ์„œ๋น„์Šค์—์„œ ๊ฐœ๋ฐœ์ ์œผ๋กœ ๊ฐ€์žฅ ์ค‘์š”ํ•˜๊ฒŒ ์ƒ๊ฐํ–ˆ๋˜ ๋ถ€๋ถ„์€ ๋‹จ์ˆœํžˆ ์›น์œผ๋กœ ๊ทธ์น˜์ง€ ์•Š๊ณ  PWA(Progressive Web App) ํ˜•ํƒœ์˜ ์„œ๋น„์Šค๋ฅผ ๋ฐฐํฌํ•˜๋Š” ๊ฒƒ์ด์—ˆ๋‹ค. ์•„์ฃผ ๋Œ€๋‹จํ•œ ๋ฌด์–ธ๊ฐ€๋ฅผ ํ•˜๋Š” ๊ฑด ์•„๋‹ˆ์ง€๋งŒ, pwa๊ฐ€ ๋ฌด์—‡์ธ์ง€ ๊ณต๋ถ€ํ•  ์ˆ˜ ์žˆ์—ˆ๊ณ  ํ‰์†Œ์—๋Š” ๊ฑฐ์˜ ์‹ ๊ฒฝ๋„ ์“ฐ์ง€ ์•Š์•˜๋˜ html์˜ head ํƒœ๊ทธ์— ๋Œ€ํ•ด์„œ๋„ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์‹ ๊ฒฝ์“ธ ์ˆ˜ ์žˆ์—ˆ๋‹ค. ๊ทธ ๋‹ค์Œ์œผ๋กœ๋Š” ์ฝ”๋“œ์˜ ์žฌํ™œ์šฉ ๋ฐ ๊ทœ์น™์„ฑ์„ ์‹ ๊ฒฝ์“ฐ๋ ค๊ณ  ํ–ˆ๋‹ค. ๋„ˆ๋ฌด ์ž‘๊ณ  ์ผ๋ฐ˜ํ™”๋œ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์•„๋‹ˆ๋ผ ์ฃผ์–ด์ง„ ์งง์€ ์‹œ๊ฐ„ ์•ˆ์—(๊ฐœ๋ฐœ ์‹œ๊ฐ„ ์ž์ฒด๋Š” ๊ฑฐ์˜ 4~5์ผ) ์ตœ๋Œ€ํ•œ ํšจ์œจ์ ์œผ๋กœ ์žฌํ™œ์šฉ ๊ฐ€๋Šฅํ•œ ์ปดํฌ๋„ŒํŠธ๋“ค์„ ์ž‘์„ฑํ•˜๋ ค ํ–ˆ๋‹ค.

๋จผ์ € PWA์— ๋Œ€ํ•œ ๋‚ด์šฉ์€ ์—ฌ๊ธฐ์— ๊ฐ„๋‹จํžˆ ์ž‘์„ฑํ•ด๋‘์—ˆ๊ณ  ์ฐธ๊ณ ์ž๋ฃŒ๋„ ๋‘์—ˆ๋‹ค.

๊ธฐ์ˆ  ์Šคํƒ

์‚ฌ์‹ค ์ฒ˜์Œ์—” ์š”์ฆ˜ ์—ด์‹ฌํžˆ ๋ฐฐ์šฐ๊ณ  ์žˆ๋Š” Next๋ฅผ ์‚ฌ์šฉํ•ด๋ณด๋ ค ํ–ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ง€๊ธˆ ํ”„๋กœ์ ํŠธ์˜ ๊ทœ๋ชจ์—์„œ ๊ตณ์ด Next๋ฅผ ์‚ฌ์šฉํ•  ํ•„์š”๊ฐ€ ์—†์„ ๊ฒƒ ๊ฐ™์•˜๊ณ , ๋„ˆ๋ฌด ์งง์€ ์‹œ๊ฐ„์ด๋ผ ๋‚ด๊ฐ€ ์ต์ˆ™ํ•œ React๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒŒ ๋‚˜์„ ๊ฒƒ ๊ฐ™๋‹ค๋Š” ํŒ๋‹จ ํ•˜์— React๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ๋กœ ํ–ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ฒ˜์Œ์—” ์•ˆ์ข‹์•„ํ–ˆ์ง€๋งŒ, ์ง€๊ธˆ์€ ๋„ˆ๋ฌด ํŽธํ•˜๊ฒŒ ์ž˜ ์“ฐ๋Š” ์ค‘์ธ styled-components, ์–ธ์ œ ์จ๋„ ์ฐธ ์ž˜ ๋งŒ๋“ค์—ˆ๋‹ค๊ณ  ์ƒ๊ฐ์ด ๋“œ๋Š” react-query๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ฐœ๋ฐœํ•˜๊ธฐ๋กœ ํ–ˆ๋‹ค. (์ด์™ธ์—๋„ axios, react-router-dom ๋“ฑ์„ ์‚ฌ์šฉํ–ˆ๋‹ค.)

๊ฐœ๋ฐœ ๊ณผ์ •

๋ธŒ๋žœ์น˜ ์ „๋žต

ํ›„โ€ฆ ๋ธŒ๋žœ์น˜ ์ „๋žต๋„ ์ •๋ง ํ•  ๋ง์ด ๋งŽ๋‹ค. ์ฒ˜์Œ์—” ๋ณดํ†ต ์ด์ „์— ํ–ˆ๋˜ ํŒ€ ํ˜‘์—…๊ณผ ๋น„์Šทํ•˜๊ฒŒ main์œผ๋กœ ๋ฐฐํฌํ•˜๊ณ , develop์— ๊ฐœ๋ฐœ ๊ธฐ๋Šฅ๋“ค์„ ๋ชจ์œผ๊ณ  ๊ฐ ๊ธฐ๋Šฅ ๋ณ„๋กœ ๋ธŒ๋žœ์น˜๋ฅผ ๋”ฐ์„œ ์ž‘์—…ํ–ˆ๋‹ค. ํ•˜์ง€๋งŒ FE ๊ฐœ๋ฐœ์ž๋Š” ๋‚˜ ํ˜ผ์ž์ธ ์ƒํƒœ์—์„œ ์ฝ”๋“œ๋ฆฌ๋ทฐ๋„ ์—†๋Š” ํ™˜๊ฒฝ์— ๋งค๋ฒˆ ๊ธฐ๋Šฅ๋“ค๋งˆ๋‹ค ๋ธŒ๋žœ์น˜๋ฅผ ๋”ฐ๊ณ  ํ˜ผ์ž ํ‘ธ์‰ฌํ•˜๊ณ  ๋จธ์ง€ํ•˜๋Š” ๊ฑด ์ƒ๊ฐ๋ณด๋‹ค ๋น„ํšจ์œจ์ ์ด๊ณ  ์˜๋ฏธ๊ฐ€ ์—†๋‹ค๊ณ  ๋Š๊ผˆ๋‹ค. ํ•ด์„œ develop ๋ธŒ๋žœ์น˜์—์„œ ๋ชจ๋“  ๊ฐœ๋ฐœ์„ ์ง„ํ–‰ํ•˜๋˜ ์ปค๋ฐ‹ ๋ฉ”์‹œ์ง€๋ฅผ ์•„์ฃผ ์ž‘๊ฒŒ์ž‘๊ฒŒ ์ž‘์„ฑํ•˜์—ฌ ์ตœ๋Œ€ํ•œ ๊ธฐ๋Šฅ ๋ณ„ ์ฝ”๋“œ๋ฅผ ๋ถ„๋ฆฌํ•˜๊ณ ์ž ํ–ˆ๋‹ค. ์ด ๋ฐฉ๋ฒ•์ด ์•„์ฃผ ์ข‹์€ ๋ฐฉ๋ฒ•์€ ์•„๋‹ˆ์ง€๋งŒ, ํ˜ผ์ž ๋น ๋ฅด๊ฒŒ ์น˜๊ณ  ๋‚˜๊ฐ€๊ธฐ์—” ๊ฐ€์žฅ ํšจ์œจ์ ์ธ ๋ฐฉ๋ฒ•๊ฐ™๋‹ค.

๊ฐœ๋ฐœ ์ˆœ์„œ

์ฒ ์ €ํžˆ ํŽ˜์ด์ง€ ๋‹จ์œ„๋กœ ์ž‘์—…์„ ํ•ด๋ณด๊ณ ์ž ํ–ˆ๋‹ค. ์‚ฌ์‹ค ๊ฐœ๋ฐœ์„ ํ•  ๋•Œ๋งˆ๋‹ค, ์–ด๋–ค ๊ธฐ์ค€์—์„œ ๋ญ๋ถ€ํ„ฐ ํ•ด์•ผํ• ์ง€ ๋ง‰๋ง‰ํ•  ๋•Œ๊ฐ€ ์ข…์ข… ์žˆ๋Š”๋ฐ, ์ด๋ฒˆ์—” ์ •๋ง ํ™•์‹คํ•˜๊ฒŒ ๊ทธ ์šฐ์„ ์ˆœ์œ„๋ฅผ ์ •ํ•˜๋ ค๊ณ  ํ–ˆ๋‹ค. ๊ฐ€๋Šฅํ•˜๋ฉด ๋ชจ๋“  ๊ธฐ์ˆ  ์Šคํƒ๊ณผ ๊ธ€๋กœ๋ฒŒ ์„ค์ •๋“ค์„ ์ฒ˜์Œ์— ๋‹ค ์žก์•„๋‘์—ˆ๊ณ (๋ผ์šฐํ„ฐ ํฌํ•จ) ์ดํ›„์— ์šฐ๋ฆฌ ์„œ๋น„์Šค์—์„œ ์ค‘์š”ํ•œ ํŽ˜์ด์ง€๋ถ€ํ„ฐ ์ณ๋‚ด๊ฐ€๋ฉด์„œ UI ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋น ๋ฅด๊ฒŒ ๊ฐœ๋ฐœํ–ˆ๋‹ค. ์ด ๋•Œ, ์Šคํ† ๋ฆฌ๋ถ์„ ์‚ฌ์šฉํ•ด์„œ ์ž‘์—…ํ•˜๋Š” ๊ฒŒ ์ •๋ง ํฐ ๋„์›€์ด ๋งŽ์ด ๋˜์—ˆ๋‹ค.(์™œ UI ํ…Œ์ŠคํŒ… ํˆด์ด๋ผ๊ณ ๋„ ๋ถ€๋ฅด๋Š”์ง€ ์•Œ๊ฒ ๋”๋ผ..!)

ํ•˜์ง€๋งŒ ์ค‘๊ฐ„์— ํŽ˜์ด์ง€ ์ปดํฌ๋„ŒํŠธ๋“ค์ด router ์„ค์ •, react-query์™€ ์ถฉ๋Œํ•˜๋Š” ๋ถ€๋ถ„๋“ค์ด ์žˆ์–ด์„œ ์Šคํ† ๋ฆฌ ์ž‘์„ฑ์„ ์ค‘๊ฐ„์— ๋ฉˆ์ท„๋‹ค. ์‹œ๊ฐ„๋งŒ ์ข€๋” ์žˆ๋‹ค๋ฉด ํ•ด๋‹น ๋‚ด์šฉ์„ ํ•ด๊ฒฐํ•˜๊ณ  ์‹ถ์€๋ฐ, ๋‹น์žฅ ๋น ๋ฅด๊ฒŒ ๊ฐœ๋ฐœํ•ด์•ผํ•˜๋Š” ์ƒํ™ฉ์—์„œ๋Š” ๊ณผ๊ฐํ•˜๊ฒŒ ์„ ํƒ๊ณผ ์ง‘์ค‘์„ ํ•˜๋Š” ๊ฒŒ ๋„์›€์ด ๋  ๊ฑฐ๋ผ ์ƒ๊ฐํ–ˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

๊ฐœ๋ฐœ ๋‚ด์šฉ

1. ์บ๋กœ์…€ ์ปดํฌ๋„ŒํŠธ

์šฐ๋ฆฌ ์„œ๋น„์Šค์˜ ๋ฉ”์ธ ํŽ˜์ด์ง€์— ๋“ค์–ด๊ฐˆ ์บ๋กœ์…€ ์Šฌ๋ผ์ด๋“œ ์ปดํฌ๋„ŒํŠธ์ด๋‹ค. ์ฒ˜์Œ์—” ๋‹น์—ฐํžˆ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๊ฐ€์ ธ๋‹ค ์“ฐ๋ ค๊ณ  ํ–ˆ๋‹ค.

carousel ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

์œ„์˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค๋ง๊ณ ๋„ UI ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ(Material, Chakra, Mantine, Ant ๋“ฑ๋“ฑ)๋„ ๊ณ ๋ คํ•ด๋ณด์•˜์ง€๋งŒ, ์ ์šฉํ•ด๋ณด๋ฉด UI๊ฐ€ ๊นจ์ง„๋‹ค๋“ ๊ฐ€ ์Šฌ๋ผ์ด๋“œ๊ฐ€ ์ œ๋Œ€๋กœ ๋˜์ง€ ์•Š๋Š”๋‹ค๋“ ๊ฐ€โ€ฆ ํ˜น์€ ๋‚ด๊ฐ€๊ฐ€ ์›ํ•˜๋Š” ์บ๋กœ์…€์ด ์•„๋‹ˆ๋“ ๊ฐ€ ๋‹ค์–‘ํ•œ ๋ฌธ์ œ๊ฐ€ ํ•˜๋‚˜์”ฉ์€ ์žˆ์—ˆ๋‹ค. ์ฐธโ€ฆ ์‚ฌ๋žŒ์ด ์‹œ๊ฐ„์ด ์—†์œผ๋ฉด ์–ด๋–ป๊ฒŒ๋“  ํ•œ๋‹ค๊ณ , ๋น ๋ฅด๊ฒŒ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ์„ ํฌ๊ธฐํ•˜๊ณ (ํ˜ผ์ž ๊ฑฐ์˜ ํ•˜๋ฃจ์ข…์ผ ์‹ธ๋งค๋‹ค๊ฐ€ ๋ˆˆ๋ฌผ์„ ๋จธ๊ธˆ๊ณ  ํฌ๊ธฐํ•จ) ์ง์ ‘ ์บ๋กœ์…€ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ž‘์„ฑํ•˜๊ธฐ๋กœ ํ–ˆ๋‹ค. ์ด์ „์— ์ง„ํ–‰ํ–ˆ๋˜ ๋‹น๊ทผ๋งˆ์ผ“ ํด๋ก  ํ”„๋กœ์ ํŠธ์—์„œ ์Šฌ๋ผ์ด๋“œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ž‘์„ฑํ•ด๋ณธ ๊ฒฝํ—˜์ด ์žˆ์–ด์„œ ์กฐ๊ธˆ ์ต์ˆ™ํ•˜๊ฒŒ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

import React, { useEffect, useRef } from 'react';
import { useTheme } from 'styled-components';

import { isMobile } from '../../utils/isMobile';
import { ReactComponent as ButtonLeft } from '../../assets/icons/common/chevron_left.svg';
import { ReactComponent as ButtonRight } from '../../assets/icons/common/chevron_right.svg';
import * as S from './Carousel.styled';

type CarouselProps = {
  children: React.ReactNode;
  currentSlideIndex: number;
  changeSlide: (index: number) => void;
};

type SlideProps = {
  children: React.ReactNode;
  isCurrentSlide?: boolean;
};

const Carousel = ({ children, currentSlideIndex, changeSlide }: CarouselProps) => {
  const touchStartPositionX = useRef(0);
  const touchEndPositionX = useRef(0);
  const handleSlideClick = (index: number) => {
    changeSlide(index);
  };

  useEffect(() => {
    const timer = setInterval(() => {
      if (currentSlideIndex >= React.Children.count(children) - 1) {
        changeSlide(0);
        return;
      }
      changeSlide(currentSlideIndex + 1);
    }, 4000);
    return () => {
      clearInterval(timer);
    };
  }, [currentSlideIndex, children]);

  const theme = useTheme();
  return (
    <S.CarouselContainer>
      <S.SlideContainer
        $currentSlideIndex={currentSlideIndex}
        onTouchStart={(e) => {
          touchStartPositionX.current = e.touches[0].clientX;
        }}
        onTouchEnd={(e) => {
          touchEndPositionX.current = e.changedTouches[0].clientX;
          const touchPositionDifference = touchStartPositionX.current - touchEndPositionX.current;

          if (touchPositionDifference > 5) {
            if (currentSlideIndex >= React.Children.count(children) - 1) {
              return;
            }
            handleSlideClick(currentSlideIndex + 1);
          }
          if (touchPositionDifference < -5) {
            if (currentSlideIndex <= 0) {
              return;
            }
            handleSlideClick(currentSlideIndex - 1);
          }
        }}
      >
        {children}
      </S.SlideContainer>
      {isMobile() || (
        <>
          {currentSlideIndex <= 0 || (
            <S.ArrowLeftButton
              onClick={() => {
                if (currentSlideIndex <= 0) {
                  return;
                }
                handleSlideClick(currentSlideIndex - 1);
              }}
            >
              <ButtonLeft fill={theme.colors.gray_05} />
            </S.ArrowLeftButton>
          )}
          {currentSlideIndex >= React.Children.count(children) - 1 || (
            <S.ArrowRightButton
              onClick={() => {
                if (currentSlideIndex >= React.Children.count(children) - 1) {
                  return;
                }
                handleSlideClick(currentSlideIndex + 1);
              }}
            >
              <ButtonRight fill={theme.colors.gray_05} />
            </S.ArrowRightButton>
          )}
        </>
      )}
    </S.CarouselContainer>
  );
};

const Slide = ({ children, isCurrentSlide }: SlideProps) => {
  return <S.SlideWrapper $isCurrentSlide={isCurrentSlide}>{children}</S.SlideWrapper>;
};

Carousel.Slide = Slide;

export default Carousel;

์ฝ”๋“œ๊ฐ€ ๋‹ค์†Œ ๊ธธ์ง€๋งŒ, ์šฐ์„  ์Šฌ๋ผ์ด๋“œ๋ฅผ ๋‹ด๊ณ  ์žˆ๋Š” Carousel ์ปดํฌ๋„ŒํŠธ์™€ ๊ทธ ๋‚ด๋ถ€์— Slide ์†์„ฑ์œผ๋กœ ์กด์žฌํ•˜๋Š” Slide ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์žˆ๋„๋ก ๊ฐœ๋ฐœํ–ˆ๋‹ค.(์ง€๊ธˆ ๋‹ค์‹œ ๋ณด๋‹ˆ 4000๊ณผ ๊ฐ™์€ ๋งค์ง ๋„˜๋ฒ„๊ฐ€ ์กด์žฌํ•˜๋„คโ€ฆ ํ—ˆํ—ฃ) ๋˜, ํ„ฐ์น˜ํ•œ ์ˆœ๊ฐ„๋ถ€ํ„ฐ ๋–ผ๊ธฐ๊นŒ์ง€์˜ ๊ฑฐ๋ฆฌ๊ฐ€ 5px ์ด์ƒ ์ฐจ์ด๊ฐ€ ๋‚˜๋ฉด ์Šฌ๋ผ์ด๋“œ๋ฅผ ๋„˜๊ธฐ๋„๋ก ๊ตฌํ˜„ํ–ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๋ฌธ์ œ๋Š” ์›น์—์„œ๋Š” touch ์ด๋ฒคํŠธ ์ž์ฒด๊ฐ€ ๋˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— isMobile()์ด๋ผ๋Š” ์œ ํ‹ธํ•จ์ˆ˜๋กœ ํ˜„์žฌ ์œ ์ €์˜ ์‹คํ–‰ ํ™˜๊ฒฝ์— ๋”ฐ๋ผ ์บ๋กœ์…€ ํ™”์‚ดํ‘œ๋ฅผ ๋ Œ๋”๋งํ•˜๋„๋ก ํ–ˆ๋‹ค.(๋ฐ์Šคํฌํƒ‘์—์„œ ๋ณผ ๋•Œ๋Š” ์–‘ ์˜†์˜ ํ™”์‚ดํ‘œ๋ฅผ ํด๋ฆญํ•˜์—ฌ ์Šฌ๋ผ์ด๋“œ๋ฅผ ์›€์ง์ผ ์ˆ˜ ์žˆ๋„๋ก!)

2. react-query service

API ํ†ต์‹ ์— ๋Œ€ํ•œ ์„ค์ •์€ apiํด๋”์—์„œ ์ž‘์„ฑํ•˜๊ณ  ๊ฐ api ์š”์ฒญ์— ๋Œ€ํ•œ ํ•จ์ˆ˜๋“ค์€ service ํด๋”์— ๋ถ„๋ฆฌํ•˜์—ฌ ์ž‘์„ฑํ•˜์˜€๋‹ค. ์˜ˆ๋ฅผ ๋“ค๋ฉด ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

api/apiConfig.ts

export const API_BASE_URL = process.env.REACT_APP_API_BASE_URL;

api/apiClient.ts

import axios from 'axios';

import { API_BASE_URL } from './apiConfig';

export const axiosFetch = axios.create({
  baseURL: API_BASE_URL,
  headers: {
    'Content-Type': 'application/json',
  },
});

service/useGetRecipe.ts

import { useQuery } from '@tanstack/react-query';
import { axiosFetch } from '../../api/apiClient';

type Flavor = {
  id: number;
  flavorName: string;
  imageUrl: string;
};

export type Recipe = {
  id: number;
  recipeName: string;
  flavors: Flavor[];
};

export const useGetRecipe = (id: number) => {
  return useQuery<{ recipe: Recipe; message: string }>(['recipe', id], async () => {
    const { data } = await axiosFetch(`/recipes/${id}`);
    const { body: recipe, message } = data;
    return { recipe, message };
  });
};

์œ„์™€ ๊ฐ™์€ ์‹์œผ๋กœ api ํ†ต์‹ ์— ๋Œ€ํ•œ ์„ค์ •๊ณผ ์‹ค์ œ api ์š”์ฒญ ํ•จ์ˆ˜๋ฅผ ๋ถ„๋ฆฌํ•˜์—ฌ ์ž‘์„ฑํ•˜์˜€๋‹ค. ์ด๋ ‡๊ฒŒ ๋ถ„๋ฆฌํ•˜๋ฉด ์ถ”ํ›„์— api ํ†ต์‹ ์— ๋Œ€ํ•œ ์„ค์ •์„ ๋ณ€๊ฒฝํ•˜๊ฑฐ๋‚˜, api ์š”์ฒญ ํ•จ์ˆ˜๋ฅผ ๋ณ€๊ฒฝํ•  ๋•Œ์—๋„ ํŽธ๋ฆฌํ•˜๊ฒŒ ์ž‘์—…ํ•  ์ˆ˜ ์žˆ๋‹ค.

3. Page ์ปดํฌ๋„ŒํŠธ

๊ฐœ๋ฐœ ์ดˆ๊ธฐ์—” ์ตœ๋Œ€ํ•œ Page ์ปดํฌ๋„ŒํŠธ๋“ค์€ api ํ†ต์‹ ํ•˜๋Š” ๋กœ์ง๊ณผ ํŽ˜์ด์ง€์— ๋Œ€ํ•œ ๋ ˆ์ด์•„์›ƒ์„ ์žก๋Š” ์šฉ๋„๋กœ๋งŒ ์กด์žฌํ•˜๊ฒŒ ํ•˜๋ ค ํ–ˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ๊ฐœ๋ฐœ์„ ํ•˜๋‹ค๋ณด๋‹ˆ ์ƒ๊ฐ๋ณด๋‹ค ์ƒํƒœ ์ž์ฒด๋ฅผ ๋Œ์–ด์˜ฌ๋ ค์•ผํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์•„์กŒ๊ณ  ๊ทธ๋Ÿฌ๋‹ค๋ณด๋‹ˆ Page ์ปดํฌ๋„ŒํŠธ๋“ค์— ์กด์žฌํ•˜๋Š” ์ฑ…์ž„๊ณ  ์—ญํ• ์ด ๋„ˆ๋ฌด ๋งŽ์•„์ง€๊ฒŒ ๋˜์—ˆ๋‹คโ€ฆ depth๊ฐ€ ๊ทธ๋ ‡๊ธฐ ๊นŠ์ง€ ์•Š์€ ์ƒํƒœ๋“ค์€ ๊ตณ์ด Context API๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์œผ๋ ค๋‹ค๋ณด๋‹ˆ๊นŒ ์ด๋Ÿฐ ์ผ์ด ์ƒ๊ธด ๊ฒƒ ๊ฐ™๋‹ค.

import React from 'react';
import { useNavigate } from 'react-router-dom';

import { routes } from '../../router';
import SubGlobalNavBar from '../../components/SubGlobalNavBar';
import FootButton from '../../components/FootButton';
import Text from '../../components/Text';
import { PROGRESS_BAR_WIDTH } from '../../constants';
import { useGetIngredients } from '../../services/useGetIngredients';
import { usePostRecipe } from '../../services/usePostRecipe';
import { useUserItem, useChangeIngredientIds } from '../../contexts/UserItemContext';
import LoadingPage from '../LoadingPage';
import * as S from './IngredientSelectPage.styled';

const IngredientSelectPage = () => {
  const { data, isLoading } = useGetIngredients();
  const { size, ingredientIds } = useUserItem();
  const changeIngredientIds = useChangeIngredientIds();
  const mutation = usePostRecipe();

  const allFlavorIdList = ingredientIds
    .map((id) => {
      const flavorIdList = data?.ingredients?.find((ingredient) => ingredient.id === id)
        ?.flavorIdList;
      return flavorIdList;
    })
    .flat();
  const flavorIdList = allFlavorIdList.filter(
    (item, index) => allFlavorIdList.indexOf(item) === index,
  );
  const isAbleToRecommend = flavorIdList.length >= size.value;
  const navigate = useNavigate();
  if (size.id === -1) {
    navigate(routes.sizePick);
  }
  return (
    <>
      {isLoading && <LoadingPage />}
      {data && (
        <S.Container>
          <S.UpperContainer>
            <S.Header>
              <SubGlobalNavBar backTo={routes.sizePick} progressWidth={PROGRESS_BAR_WIDTH.MIDDLE} />
            </S.Header>
            <S.Main>
              <Text size="large">์ข‹์•„ํ•˜๋Š” ์žฌ๋ฃŒ๋ฅผ ์„ ํƒํ•ด์ฃผ์„ธ์š”</Text>
              <S.IngredientsContainer>
                {data?.ingredients?.map((ingredient) => (
                  <S.IngredientWrapper
                    key={ingredient.id}
                    onClick={() => {
                      if (ingredientIds.includes(ingredient.id)) {
                        changeIngredientIds(ingredientIds.filter((id) => id !== ingredient.id));
                      } else {
                        changeIngredientIds([...ingredientIds, ingredient.id]);
                      }
                    }}
                  >
                    <S.IngredientImage
                      src={ingredient.imageUrl}
                      alt={ingredient.name}
                      $isClicked={ingredientIds.includes(ingredient.id)}
                    />
                    <S.IngredientName>{ingredient.name}</S.IngredientName>
                  </S.IngredientWrapper>
                ))}
              </S.IngredientsContainer>
            </S.Main>
          </S.UpperContainer>
          <S.Footer>
            <S.BottomContainer>
              {isAbleToRecommend || ingredientIds.length === 0 || (
                <Text size="small">์žฌ๋ฃŒ๋ฅผ ์กฐ๊ธˆ ๋” ๊ณจ๋ผ๋ณผ๊นŒ์š”?</Text>
              )}
              <FootButton
                onClick={() => {
                  mutation.mutate({
                    sizeId: size.id,
                    ingredientIds,
                  });
                }}
                disabled={!isAbleToRecommend}
              >
                ๊ฒฐ๊ณผ๋ณด๊ธฐ
              </FootButton>
            </S.BottomContainer>
          </S.Footer>
        </S.Container>
      )}
    </>
  );
};

export default IngredientSelectPage;
4. Context API์™€ useState

์›ํ‹ฐ๋“œ ์ธํ„ด์‹ญ์„ ํ†ตํ•ด ๋ฐฐ์šด ๊ฒŒ ์žˆ์—ˆ๋‹ค. Context API๋Š” ์ „์—ญ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ api๊ฐ€ ์•„๋‹ˆ๋ผ๋Š” ๊ฒƒ์ด๋‹ค. ์ •ํ™•ํžˆ ๋งํ•˜๋ฉด ์ปดํฌ๋„ŒํŠธ๋“ค์˜ ์ง‘ํ•ฉ์— ์–ด๋–ค ๋ฌธ๋งฅ(Context)์„ ์ „๋‹ฌํ•˜๋Š” ์šฉ๋„์ด์ง€, ๊ทธ ํ˜ผ์ž์„œ๋Š” ์ „์—ญ ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ํ•  ์ˆ˜ ์—†๋‹ค๋Š” ์˜๋ฏธ์˜€๋‹ค. ์ฒ˜์Œ์—” ์ „์—ญ ์ƒํƒœ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ• ๊นŒ ํ•˜๋‹ค๊ฐ€, ๋ Œ๋”๋ง ํšจ์œจ์ด๋‚˜ ์ตœ์ ํ™”๊ฐ€ ์ข‹์ง„ ์•Š๋”๋ผ๋„ Context API์™€ useState๋ฅผ ํ†ตํ•œ ์ƒํƒœ๊ด€๋ฆฌ๋ฅผ ์ง์ ‘ ๋Š๊ปด๋ณด๊ณ  ์‹ถ์–ด์„œ Context API์™€ useState๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ „์—ญ ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ์ง„ํ–‰ํ–ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ด๋ฒˆ ํ”„๋กœ์ ํŠธ๋ฅผ ํ†ตํ•ด Context API์™€ useState๋ฅผ ์‚ฌ์šฉํ•œ ์ „์—ญ ์ƒํƒœ ๊ด€๋ฆฌ๊ฐ€ ์–ด๋–ค ์‹์œผ๋กœ ์ด๋ฃจ์–ด์ง€๋Š”์ง€ ๋งŽ์ด ๋ฐฐ์šธ ์ˆ˜ ์žˆ์—ˆ๋‹ค.(์ฝ”๋“œ๊ฐ€ ์ง€์ €๋ถ„ํ•˜๊ธด ํ•˜๋‹ค.)

์•„๋ž˜๋Š” ์œ ์ €๊ฐ€ ์„ ํƒํ•œ ๊ฐ’๋“ค์„ ์ „์—ญ์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ Context ์ฝ”๋“œ์ด๋‹ค.

import React, { createContext, useContext, useState } from 'react';

type Size = {
  id: number;
  value: number;
};

type UserItem = {
  size: Size;
  ingredientIds: number[];
};

export const userItemContext = createContext<UserItem>({
  size: { id: -1, value: -1 },
  ingredientIds: [],
});
export const changeSizeContext = createContext(({ id, value }: Size) => {
  console.error(`changeSizeContext๋ฅผ ๋ฒ—์–ด๋‚ฌ์Šต๋‹ˆ๋‹ค. size: {id: ${id}, value: ${value}}`);
});
export const changeIngredientIdsContext = createContext((ingredientIds: number[]) => {
  console.error(`changeIngredientIdsContext๋ฅผ ๋ฒ—์–ด๋‚ฌ์Šต๋‹ˆ๋‹ค. ingredientIds: ${ingredientIds}`);
});
export const initUserItemContext = createContext(() => {
  console.error('initUserItemContext๋ฅผ ๋ฒ—์–ด๋‚ฌ์Šต๋‹ˆ๋‹ค.');
});

export const useUserItem = () => useContext(userItemContext);
export const useChangeSize = () => useContext(changeSizeContext);
export const useChangeIngredientIds = () => useContext(changeIngredientIdsContext);
export const useInitUserItem = () => useContext(initUserItemContext);

type Props = {
  children: React.ReactNode;
};

export const UserItemProvider = ({ children }: Props) => {
  const [userItem, setUserItem] = useState<UserItem>({
    size: { id: -1, value: -1 },
    ingredientIds: [],
  });

  const changeSize = ({ id, value }: Size) => {
    setUserItem({
      ...userItem,
      size: { id, value },
    });
  };

  const changeIngredientIds = (ingredientIds: number[]) => {
    setUserItem({
      ...userItem,
      ingredientIds,
    });
  };

  const initUserItem = () => {
    setUserItem({
      size: { id: -1, value: -1 },
      ingredientIds: [],
    });
  };

  return (
    <userItemContext.Provider value={userItem}>
      <changeSizeContext.Provider value={changeSize}>
        <changeIngredientIdsContext.Provider value={changeIngredientIds}>
          <initUserItemContext.Provider value={initUserItem}>
            {children}
          </initUserItemContext.Provider>
        </changeIngredientIdsContext.Provider>
      </changeSizeContext.Provider>
    </userItemContext.Provider>
  );
};

4. ๋งˆ๋ฌด๋ฆฌ

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

๋กœ๊ทธ์ธ(์นด์นด์˜ค ํ˜น์€ ๋„ค์ด๋ฒ„)
๋ถ๋งˆํฌ ๊ธฐ๋Šฅ
์˜จ๋ณด๋”ฉ ํŽ˜์ด์ง€(๋ถ๋งˆํฌ๋ฅผ ํ†ตํ•œ ์•ฑ ๊ฒฝํ—˜ ์•ˆ๋‚ด๊ฐ€ ๋  ๊ฒƒ ๊ฐ™์•„์š”)
์ฃผ์œ„ ๋ฐฐ์Šคํ‚จ๋ผ๋นˆ์Šค ์œ„์น˜ ์ œ๊ณต ํŽ˜์ด์ง€
๊ธฐ์กด ์ฝ”๋“œ ๋ฆฌํŒฉํ† ๋ง(์ฝ”๋“œ ์ •๋ฆฌ)