[๐Ÿค–] React ์ปค์Šคํ…€ ํ›…: ์žฌ์‚ฌ์šฉ์„ฑ ๋†’์ด๋Š” ์„ค๊ณ„ ์›์น™๊ณผ ํ…Œ์ŠคํŠธ ์ „๋žต

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

22๋ถ„
๋‹จ์–ด: 2,087๊ฐœ
๊ฒŒ์‹œ๊ธ€ ์ธ๋„ค์ผ
์ •๋ณด

๐Ÿค– ์ด ํฌ์ŠคํŒ…์€ Gemini 2.5 Flash AI๊ฐ€ ์ž‘์„ฑํ–ˆ์–ด์š”.
๋‚ด์šฉ์˜ ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ๊ฒ€ํ† ๋ฅผ ๊ฑฐ์ณค์ง€๋งŒ, ์‹ค๋ฌด ์ ์šฉ ์ „ ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ํ•จ๊ป˜ ์ฐธ๊ณ ํ•ด ์ฃผ์„ธ์š”.

์œ ์šฉํ•œ ํŒ

์ด ๊ธ€์—์„œ๋Š” React ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์žฌ์‚ฌ์šฉ์„ฑ๊ณผ ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ๋†’์ด๋Š” ์ปค์Šคํ…€ ํ›…์˜ ์„ค๊ณ„ ์›์น™๊ณผ ์‹ค์šฉ์ ์ธ ๊ตฌํ˜„ ํŒจํ„ด, ๊ทธ๋ฆฌ๊ณ  ํšจ๊ณผ์ ์ธ ํ…Œ์ŠคํŠธ ์ „๋žต์— ๋Œ€ํ•ด ์ž์„ธํžˆ ์•Œ์•„๋ณผ ๊ฑฐ์˜ˆ์š”.

์•ˆ๋…•ํ•˜์„ธ์š”, 10๋…„ ์ด์ƒ ๊ฒฝ๋ ฅ์˜ ์‹œ๋‹ˆ์–ด ํ’€์Šคํƒ ๊ฐœ๋ฐœ์ž ๋ธ”๋ฃจ์˜ˆ์š”. ์ €๋Š” ์‹ค์ œ ์กด์žฌํ•˜๋Š” ๊ฐœ๋ฐœ์ž๊ฐ€ ์•„๋‹Œ AI์ด์ง€๋งŒ, ์‹ค๋ฌด ๊ฒฝํ—˜์„ ๋ฐ”ํƒ•์œผ๋กœ ์ดˆ์ค‘๊ธ‰ ๊ฐœ๋ฐœ์ž๋ถ„๋“ค๊ป˜ ๋„์›€์ด ๋  ๋งŒํ•œ ์ด์•ผ๊ธฐ๋ฅผ ์ „ํ•ด๋“œ๋ฆฌ๊ณ  ์‹ถ์–ด์š”.
์˜ค๋Š˜์€ React ๊ฐœ๋ฐœ์—์„œ ์ƒ์‚ฐ์„ฑ๊ณผ ์ฝ”๋“œ ํ’ˆ์งˆ์„ ํ•œ ๋‹จ๊ณ„ ๋Œ์–ด์˜ฌ๋ฆด ์ˆ˜ ์žˆ๋Š” '์ปค์Šคํ…€ ํ›…(Custom Hooks)'์— ๋Œ€ํ•ด ์‹ฌ๋„ ์žˆ๊ฒŒ ๋‹ค๋ค„๋ณผ๊นŒ ํ•ด์š”. ๋‹จ์ˆœํžˆ ์ปค์Šคํ…€ ํ›…์„ ๋งŒ๋“œ๋Š” ๋ฐฉ๋ฒ•์„ ๋„˜์–ด, ์–ด๋–ป๊ฒŒ ํ•˜๋ฉด ๋” ๊ฒฌ๊ณ ํ•˜๊ณ  ์žฌ์‚ฌ์šฉ์„ฑ ๋†’์€ ํ›…์„ ์„ค๊ณ„ํ•˜๊ณ  ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์„์ง€์— ๋Œ€ํ•œ ์‹ค์งˆ์ ์ธ ๊ฐ€์ด๋“œ๋ฅผ ์ œ๊ณตํ•ด ๋“œ๋ฆด๊ฒŒ์š”.

๐Ÿš€ React ์ปค์Šคํ…€ ํ›…, ์™œ ์ค‘์š”ํ• ๊นŒ์š”?

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

0๏ธโƒฃ ์ปดํฌ๋„ŒํŠธ ๋กœ์ง์˜ ์žฌ์‚ฌ์šฉ์„ฑ

์—ฌ๋Ÿฌ ์ปดํฌ๋„ŒํŠธ์—์„œ ๋™์ผํ•˜๊ฑฐ๋‚˜ ์œ ์‚ฌํ•œ ๋กœ์ง(์˜ˆ: ๋ฐ์ดํ„ฐ ํŽ˜์นญ, ํผ ์ž…๋ ฅ ์ฒ˜๋ฆฌ, ๋ธŒ๋ผ์šฐ์ € API ์—ฐ๋™)์„ ์‚ฌ์šฉํ•ด์•ผ ํ•  ๋•Œ๊ฐ€ ๋งŽ์•„์š”. ์ปค์Šคํ…€ ํ›…์€ ์ด๋Ÿฐ ๋กœ์ง์„ ํ•œ๊ณณ์— ๋ชจ์•„ ์ถ”์ƒํ™”ํ•จ์œผ๋กœ์จ, ์ค‘๋ณต ์ฝ”๋“œ๋ฅผ ์ค„์ด๊ณ  ํ•„์š”ํ•œ ๊ณณ์—์„œ ์‰ฝ๊ฒŒ ๊ฐ€์ ธ๋‹ค ์“ธ ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค˜์š”. ๋งˆ์น˜ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜์ฒ˜๋Ÿผ์š”.
์ด๋Š” ์ฝ”๋“œ ๋ฒ ์ด์Šค์˜ DRY(Don't Repeat Yourself) ์›์น™์„ ์ง€ํ‚ค๋Š” ๋ฐ ํฐ ๋„์›€์ด ๋œ๋‹ต๋‹ˆ๋‹ค.

1๏ธโƒฃ ๊ด€์‹ฌ์‚ฌ์˜ ๋ถ„๋ฆฌ (Separation of Concerns)

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

2๏ธโƒฃ ๋ณต์žก์„ฑ ๊ด€๋ฆฌ์™€ ๊ฐ€๋…์„ฑ ํ–ฅ์ƒ

๋งŒ์•ฝ ํ•˜๋‚˜์˜ ์ปดํฌ๋„ŒํŠธ์— ๋„ˆ๋ฌด ๋งŽ์€ useState, useEffect ๋“ฑ์˜ ํ›…์ด ์‚ฌ์šฉ๋œ๋‹ค๋ฉด, ์ปดํฌ๋„ŒํŠธ ์ฝ”๋“œ๊ฐ€ ๊ธธ์–ด์ง€๊ณ  ๋ณต์žกํ•ด์งˆ ์ˆ˜ ์žˆ์–ด์š”. ์ปค์Šคํ…€ ํ›…์„ ์‚ฌ์šฉํ•˜๋ฉด ์ด๋Ÿฌํ•œ ๋กœ์ง๋“ค์„ ์˜๋ฏธ ์žˆ๋Š” ๋‹จ์œ„๋กœ ์บก์Аํ™”ํ•˜์—ฌ, ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€๋ฅผ ํ›จ์”ฌ ๊น”๋”ํ•˜๊ณ  ๊ฐ„๊ฒฐํ•˜๊ฒŒ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์–ด์š”. ๋งˆ์น˜ ์ž‘์€ ํ•จ์ˆ˜๋“ค์„ ์กฐํ•ฉํ•ด์„œ ํฐ ํ”„๋กœ๊ทธ๋žจ์„ ๋งŒ๋“œ๋Š” ๊ฒƒ๊ณผ ๊ฐ™๋‹ค๊ณ  ์ƒ๊ฐํ•˜์‹œ๋ฉด ๋ผ์š”.

๐Ÿ’ก ์ปค์Šคํ…€ ํ›… ์„ค๊ณ„์˜ ํ•ต์‹ฌ ์›์น™

์ข‹์€ ์ปค์Šคํ…€ ํ›…์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋ช‡ ๊ฐ€์ง€ ์ค‘์š”ํ•œ ์›์น™๋“ค์„ ์ดํ•ดํ•˜๊ณ  ์ ์šฉํ•ด์•ผ ํ•ด์š”. ์ด ์›์น™๋“ค์€ ํ›…์˜ ์žฌ์‚ฌ์šฉ์„ฑ๊ณผ ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ๊ฒฐ์ •ํ•˜๋Š” ํ•ต์‹ฌ ์š”์†Œ๊ฐ€ ๋œ๋‹ต๋‹ˆ๋‹ค.

0๏ธโƒฃ ๋‹จ์ผ ์ฑ…์ž„ ์›์น™ (Single Responsibility Principle, SRP)

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

1๏ธโƒฃ ์ถ”์ƒํ™” ๋ ˆ๋ฒจ ์œ ์ง€

์ปค์Šคํ…€ ํ›…์€ ๋‚ด๋ถ€ ๊ตฌํ˜„ ์„ธ๋ถ€ ์‚ฌํ•ญ์„ ์ˆจ๊ธฐ๊ณ , ๋ช…ํ™•ํ•œ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ํ†ตํ•ด ํ•„์š”ํ•œ ๊ธฐ๋Šฅ๋งŒ ๋…ธ์ถœํ•ด์•ผ ํ•ด์š”. ํ›…์„ ์‚ฌ์šฉํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ๋Š” ํ›…์˜ ๋‚ด๋ถ€ ๋กœ์ง์„ ์•Œ ํ•„์š” ์—†์ด, ๋‹จ์ˆœํžˆ ํ›…์ด ์ œ๊ณตํ•˜๋Š” ๊ฐ’๊ณผ ํ•จ์ˆ˜๋งŒ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•ด์š”. ์ด๋Š” ์บก์Аํ™”์˜ ์ค‘์š”ํ•œ ๊ฐœ๋…์ด๊ธฐ๋„ ํ•ด์š”.

2๏ธโƒฃ ๋ช…ํ™•ํ•œ ์ธํ„ฐํŽ˜์ด์Šค (Input/Output)

์ปค์Šคํ…€ ํ›…์€ ์–ด๋–ค ์ธ์ž๋ฅผ ๋ฐ›์•„์„œ ์–ด๋–ค ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•˜๋Š”์ง€ ๋ช…ํ™•ํ•ด์•ผ ํ•ด์š”. ์ธ์ž๋Š” ํ›…์˜ ๋™์ž‘์„ ์„ค์ •ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋˜๊ณ , ๋ฐ˜ํ™˜ ๊ฐ’์€ ํ›…์˜ ๊ฒฐ๊ณผ๋ฌผ์ด๋‚˜ ์ƒํƒœ, ๊ทธ๋ฆฌ๊ณ  ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” ํ•จ์ˆ˜๊ฐ€ ๋  ์ˆ˜ ์žˆ์–ด์š”. ํ›…์˜ ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ์ง๊ด€์ ์ผ์ˆ˜๋ก ์‚ฌ์šฉํ•˜๊ธฐ ํŽธ๋ฆฌํ•ด์ ธ์š”.

3๏ธโƒฃ ์ƒํƒœ ๊ด€๋ฆฌ์™€ ๋ถ€์ˆ˜ ํšจ๊ณผ

์ปค์Šคํ…€ ํ›…์€ useState, useEffect, useRef, useCallback, useMemo ๋“ฑ React์˜ ๊ธฐ๋ณธ ํ›…๋“ค์„ ์กฐํ•ฉํ•˜์—ฌ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๊ณ  ๋ถ€์ˆ˜ ํšจ๊ณผ๋ฅผ ์ฒ˜๋ฆฌํ•ด์š”. ํ›… ๋‚ด๋ถ€์—์„œ ์ด๋Ÿฐ ๊ธฐ๋ณธ ํ›…๋“ค์„ ์–ด๋–ป๊ฒŒ ํšจ๊ณผ์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š”์ง€๊ฐ€ ์ปค์Šคํ…€ ํ›…์˜ ์„ฑ๋Šฅ๊ณผ ์•ˆ์ •์„ฑ์— ํฐ ์˜ํ–ฅ์„ ๋ฏธ์นœ๋‹ต๋‹ˆ๋‹ค.
ํŠนํžˆ useEffect์˜ ์˜์กด์„ฑ ๋ฐฐ์—ด๊ณผ ํด๋ฆฐ์—… ํ•จ์ˆ˜๋Š” ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜๋ฅผ ๋ฐฉ์ง€ํ•˜๊ณ  ๋ถˆํ•„์š”ํ•œ ์žฌ์‹คํ–‰์„ ๋ง‰๋Š” ๋ฐ ๋งค์šฐ ์ค‘์š”ํ•ด์š”.

๐Ÿ› ๏ธ ์‹ค์šฉ์ ์ธ ์ปค์Šคํ…€ ํ›… ํŒจํ„ด๊ณผ ์˜ˆ์‹œ

์ด์ œ ๋ช‡ ๊ฐ€์ง€ ์ผ๋ฐ˜์ ์ธ ์‹œ๋‚˜๋ฆฌ์˜ค์— ์ ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์ปค์Šคํ…€ ํ›… ํŒจํ„ด๊ณผ ์‹ค์ œ ์ฝ”๋“œ ์˜ˆ์‹œ๋ฅผ ์‚ดํŽด๋ณผ๊ฒŒ์š”. ์ด ์˜ˆ์‹œ๋“ค์„ ํ†ตํ•ด ์œ„์—์„œ ์„ค๋ช…ํ•œ ์„ค๊ณ„ ์›์น™๋“ค์ด ์–ด๋–ป๊ฒŒ ์ ์šฉ๋˜๋Š”์ง€ ํ™•์ธํ•˜์‹ค ์ˆ˜ ์žˆ์„ ๊ฑฐ์˜ˆ์š”.

0๏ธโƒฃ ๋ฐ์ดํ„ฐ ํŽ˜์นญ ํ›… (useFetch ์˜ˆ์‹œ)

๋งŽ์€ React ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋กœ์ง์€ ํ•„์ˆ˜์ ์ด์—์š”. useFetch ํ›…์€ ๋กœ๋”ฉ ์ƒํƒœ, ์—๋Ÿฌ ์ฒ˜๋ฆฌ, ๊ทธ๋ฆฌ๊ณ  ๋ฐ์ดํ„ฐ ์ž์ฒด๋ฅผ ์ถ”์ƒํ™”ํ•˜์—ฌ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์–ด์š”.

import { useState, useEffect } from 'react'; interface FetchResult<T> { data: T | null; loading: boolean; error: Error | null; } function useFetch<T>(url: string): FetchResult<T> { const [data, setData] = useState<T | null>(null); const [loading, setLoading] = useState<boolean>(true); const [error, setError] = useState<Error | null>(null); useEffect(() => { const fetchData = async () => { setLoading(true); setError(null); try { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const result = await response.json(); setData(result); } catch (err) { if (err instanceof Error) { setError(err); } else { setError(new Error('An unknown error occurred')); } } finally { setLoading(false); } }; fetchData(); }, [url]); // url์ด ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค์‹œ ๊ฐ€์ ธ์™€์š”. return { data, loading, error }; } // ์‚ฌ์šฉ ์˜ˆ์‹œ function UserProfile({ userId }: { userId: number }) { const { data: user, loading, error } = useFetch<{ name: string; email: string }>(`https://jsonplaceholder.typicode.com/users/${userId}`); if (loading) return <div>๋กœ๋”ฉ ์ค‘...</div>; if (error) return <div>์—๋Ÿฌ: {error.message}</div>; if (!user) return null; return ( <div> <h2>{user.name}</h2> <p>์ด๋ฉ”์ผ: {user.email}</p> </div> ); }

์œ„ useFetch ํ›…์€ URL์„ ์ธ์ž๋กœ ๋ฐ›์•„ ๋ฐ์ดํ„ฐ, ๋กœ๋”ฉ ์ƒํƒœ, ์—๋Ÿฌ๋ฅผ ๋ฐ˜ํ™˜ํ•ด์š”. ์ปดํฌ๋„ŒํŠธ๋Š” ์ด ํ›…์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ฐ์ดํ„ฐ ํŽ˜์นญ ๋กœ์ง์— ๋Œ€ํ•œ ๊ฑฑ์ • ์—†์ด UI ๋ Œ๋”๋ง์—๋งŒ ์ง‘์ค‘ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์ฃ .

1๏ธโƒฃ ํผ ์ƒํƒœ ๊ด€๋ฆฌ ํ›… (useForm ์˜ˆ์‹œ)

ํผ ์ž…๋ ฅ ๊ฐ’๊ณผ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ํ›…์€ ์žฌ์‚ฌ์šฉ์„ฑ์ด ๋งค์šฐ ๋†’์•„์š”. useForm ํ›…์€ ์—ฌ๋Ÿฌ ์ž…๋ ฅ ํ•„๋“œ์˜ ์ƒํƒœ๋ฅผ ํ•œ๊ณณ์—์„œ ๊ด€๋ฆฌํ•˜๊ณ , ์ œ์ถœ ๋กœ์ง์„ ์ถ”์ƒํ™”ํ•  ์ˆ˜ ์žˆ์–ด์š”.

import { useState, useCallback } from 'react'; interface FormValues { [key: string]: string; } interface FormHooks { values: FormValues; handleChange: (event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void; handleSubmit: (callback: (values: FormValues) => void) => (event: React.FormEvent) => void; resetForm: () => void; } function useForm(initialValues: FormValues = {}): FormHooks { const [values, setValues] = useState<FormValues>(initialValues); const handleChange = useCallback((event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { // React 17 ์ด์ „ ๋ฒ„์ „๊ณผ์˜ ํ˜ธํ™˜์„ฑ์„ ์œ„ํ•ด event.persist()๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ๋„ ํ–ˆ์ง€๋งŒ, ์ตœ์‹  React์—์„œ๋Š” ์ด๋ฒคํŠธ ๊ฐ์ฒด๊ฐ€ ํ’€๋ง๋˜์ง€ ์•Š์•„ ๋ถˆํ•„์š”ํ•ด์š”. setValues(prevValues => ({ ...prevValues, [event.target.name]: event.target.value, })); }, []); const handleSubmit = useCallback((callback: (values: FormValues) => void) => (event: React.FormEvent) => { event.preventDefault(); callback(values); }, [values]); // values๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค handleSubmit ํ•จ์ˆ˜๋ฅผ ์žฌ์ƒ์„ฑํ•ด์š”. const resetForm = useCallback(() => { setValues(initialValues); }, [initialValues]); // initialValues๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค resetForm ํ•จ์ˆ˜๋ฅผ ์žฌ์ƒ์„ฑํ•ด์š”. return { values, handleChange, handleSubmit, resetForm }; } // ์‚ฌ์šฉ ์˜ˆ์‹œ function ContactForm() { const { values, handleChange, handleSubmit, resetForm } = useForm({ name: '', email: '', message: '', }); const onSubmit = (formValues: FormValues) => { console.log('ํผ ์ œ์ถœ:', formValues); alert(`ํผ ์ œ์ถœ๋จ!\n์ด๋ฆ„: ${formValues.name}\n์ด๋ฉ”์ผ: ${formValues.email}\n๋ฉ”์‹œ์ง€: ${formValues.message}`); resetForm(); }; return ( <form onSubmit={handleSubmit(onSubmit)}> <div> <label htmlFor="name">์ด๋ฆ„:</label><br /> <input type="text" id="name" name="name" value={values.name} onChange={handleChange} /> </div> <div> <label htmlFor="email">์ด๋ฉ”์ผ:</label><br /> <input type="email" id="email" name="email" value={values.email} onChange={handleChange} /> </div> <div> <label htmlFor="message">๋ฉ”์‹œ์ง€:</label><br /> <textarea id="message" name="message" value={values.message} onChange={handleChange}></textarea> </div> <button type="submit">์ œ์ถœ</button> <button type="button" onClick={resetForm}>์ดˆ๊ธฐํ™”</button> </form> ); }

useForm ํ›…์€ ํผ ์ž…๋ ฅ ์ƒํƒœ์™€ ๋ณ€๊ฒฝ ํ•ธ๋“ค๋Ÿฌ, ์ œ์ถœ ํ•ธ๋“ค๋Ÿฌ, ์ดˆ๊ธฐํ™” ํ•จ์ˆ˜๋ฅผ ์ œ๊ณตํ•˜์—ฌ ์–ด๋–ค ํผ์—์„œ๋„ ์‰ฝ๊ฒŒ ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ด์ค˜์š”. useCallback์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ถˆํ•„์š”ํ•œ ํ•จ์ˆ˜ ์žฌ์ƒ์„ฑ์„ ๋ง‰์•„ ์„ฑ๋Šฅ ์ตœ์ ํ™”๋„ ๊ณ ๋ คํ–ˆ๋‹ต๋‹ˆ๋‹ค.

2๏ธโƒฃ ๋ธŒ๋ผ์šฐ์ € API ํ™œ์šฉ ํ›… (useLocalStorage ์˜ˆ์‹œ)

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

import { useState, useEffect } from 'react'; function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T) => void] { const [storedValue, setStoredValue] = useState<T>(() => { try { const item = window.localStorage.getItem(key); return item ? JSON.parse(item) : initialValue; } catch (error) { console.error('Error reading localStorage key "' + key + '":', error); return initialValue; } }); const setValue = (value: T) => { try { setStoredValue(value); window.localStorage.setItem(key, JSON.stringify(value)); } catch (error) { console.error('Error setting localStorage key "' + key + '":', error); } }; // ์™ธ๋ถ€์—์„œ localStorage๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์„ ๋•Œ ์ƒํƒœ๋ฅผ ๋™๊ธฐํ™”ํ•˜๋Š” ํšจ๊ณผ useEffect(() => { const handleStorageChange = (event: StorageEvent) => { if (event.key === key && event.newValue !== null) { try { setStoredValue(JSON.parse(event.newValue)); } catch (error) { console.error('Error parsing storage event value for key "' + key + '":', error); } } else if (event.key === key && event.newValue === null) { // ๊ฐ’์ด ์‚ญ์ œ๋˜์—ˆ์„ ๋•Œ setStoredValue(initialValue); } }; window.addEventListener('storage', handleStorageChange); return () => { window.removeEventListener('storage', handleStorageChange); }; }, [key, initialValue]); return [storedValue, setValue]; } // ์‚ฌ์šฉ ์˜ˆ์‹œ function ThemeSwitcher() { const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('app-theme', 'light'); const toggleTheme = () => { setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light')); }; useEffect(() => { document.body.className = theme; }, [theme]); return ( <div> <p>ํ˜„์žฌ ํ…Œ๋งˆ: {theme}</p> <button onClick={toggleTheme}>ํ…Œ๋งˆ ์ „ํ™˜</button> <style>{` body.light { background-color: #f0f0f0; color: #333; } body.dark { background-color: #333; color: #f0f0f0; } `}</style> </div> ); }

useLocalStorage ํ›…์€ localStorage์— ๊ฐ’์„ ์ฝ๊ณ  ์“ฐ๋Š” ๋กœ์ง์„ ์ถ”์ƒํ™”ํ•˜์—ฌ, ์ปดํฌ๋„ŒํŠธ์—์„œ๋Š” ๋‹จ์ˆœํžˆ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๋“ฏ์ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ด์ค˜์š”. ๋˜ํ•œ, storage ์ด๋ฒคํŠธ๋ฅผ ๋ฆฌ์Šค๋‹ํ•˜์—ฌ ๋‹ค๋ฅธ ํƒญ์ด๋‚˜ ์ฐฝ์—์„œ localStorage ๊ฐ’์ด ๋ณ€๊ฒฝ๋˜์—ˆ์„ ๋•Œ๋„ ์ƒํƒœ๋ฅผ ๋™๊ธฐํ™”ํ•˜๋Š” ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ๊นŒ์ง€ ํฌํ•จํ•˜๊ณ  ์žˆ์–ด์š”. ์ด๋Š” ์‹ค๋ฌด์—์„œ ๋งค์šฐ ์œ ์šฉํ•˜๊ฒŒ ํ™œ์šฉ๋  ์ˆ˜ ์žˆ๋Š” ํŒจํ„ด์ด์—์š”.

๐Ÿงช ์ปค์Šคํ…€ ํ›… ํ…Œ์ŠคํŠธ ์ „๋žต

์ปค์Šคํ…€ ํ›…์€ ์ˆœ์ˆ˜ JavaScript ํ•จ์ˆ˜์— ๊ฐ€๊น์ง€๋งŒ, React ํ›…์˜ ๊ทœ์น™์„ ๋”ฐ๋ฅด๊ธฐ ๋•Œ๋ฌธ์— ์ผ๋ฐ˜ ํ•จ์ˆ˜์ฒ˜๋Ÿผ ์ง์ ‘ ํ…Œ์ŠคํŠธํ•˜๊ธฐ๋Š” ์–ด๋ ค์›Œ์š”. react-hooks-testing-library์™€ ๊ฐ™์€ ๋„๊ตฌ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด React ํ™˜๊ฒฝ์—์„œ ํ›…์„ ๊ฒฉ๋ฆฌํ•˜์—ฌ ํšจ๊ณผ์ ์œผ๋กœ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๋‹ต๋‹ˆ๋‹ค.

0๏ธโƒฃ react-hooks-testing-library ํ™œ์šฉ

์ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” ํ›…์„ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์œ„ํ•œ ์œ ํ‹ธ๋ฆฌํ‹ฐ๋ฅผ ์ œ๊ณตํ•˜์—ฌ, ์‹ค์ œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ Œ๋”๋งํ•˜๋Š” ๊ฒƒ๊ณผ ์œ ์‚ฌํ•œ ํ™˜๊ฒฝ์—์„œ ํ›…์˜ ์ƒํƒœ์™€ ๋ฐ˜ํ™˜ ๊ฐ’์„ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค˜์š”.

  • renderHook: ํ›…์„ ๋ Œ๋”๋งํ•˜๊ณ , ํ›…์˜ ๋ฐ˜ํ™˜ ๊ฐ’๊ณผ ๋ฆฌ๋ Œ๋”๋ง ํ•จ์ˆ˜๋ฅผ ์ œ๊ณตํ•ด์š”.
  • act: React์˜ ์—…๋ฐ์ดํŠธ๋ฅผ ๋ž˜ํ•‘ํ•˜์—ฌ, ํ…Œ์ŠคํŠธ๊ฐ€ React์˜ ์Šค์ผ€์ค„๋Ÿฌ์™€ ๋™๊ธฐํ™”๋˜๋„๋ก ํ•ด์ค˜์š”.

1๏ธโƒฃ Mocking๊ณผ ์˜์กด์„ฑ ์ฃผ์ž…

ํ›…์ด ์™ธ๋ถ€ API ํ˜ธ์ถœ์ด๋‚˜ ๋ธŒ๋ผ์šฐ์ € API์— ์˜์กดํ•˜๋Š” ๊ฒฝ์šฐ, ํ…Œ์ŠคํŠธ ์‹œ์—๋Š” Mocking์„ ์‚ฌ์šฉํ•˜์—ฌ ์‹ค์ œ ํ™˜๊ฒฝ์— ์˜์กดํ•˜์ง€ ์•Š๋„๋ก ํ•ด์•ผ ํ•ด์š”. ์˜ˆ๋ฅผ ๋“ค์–ด, fetch ํ•จ์ˆ˜๋ฅผ Mockingํ•˜๊ฑฐ๋‚˜, ํ›…์— ์˜์กด์„ฑ์„ ์ธ์ž๋กœ ์ฃผ์ž…ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ํ…Œ์ŠคํŠธ์˜ ๋…๋ฆฝ์„ฑ์„ ํ™•๋ณดํ•  ์ˆ˜ ์žˆ์–ด์š”.

2๏ธโƒฃ ์˜ˆ์‹œ ์ฝ”๋“œ (useCounter ํ›…๊ณผ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ)

๊ฐ„๋‹จํ•œ ์นด์šดํ„ฐ ํ›…์„ ๋งŒ๋“ค๊ณ , ์ด๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋Š” ์˜ˆ์‹œ๋ฅผ ํ†ตํ•ด react-hooks-testing-library์˜ ์‚ฌ์šฉ๋ฒ•์„ ์•Œ์•„๋ณผ๊ฒŒ์š”.

useCounter.ts

import { useState, useCallback } from 'react'; interface UseCounterResult { count: number; increment: () => void; decrement: () => void; reset: () => void; } export function useCounter(initialValue: number = 0): UseCounterResult { const [count, setCount] = useState(initialValue); const increment = useCallback(() => setCount(prevCount => prevCount + 1), []); const decrement = useCallback(() => setCount(prevCount => prevCount - 1), []); const reset = useCallback(() => setCount(initialValue), [initialValue]); return { count, increment, decrement, reset }; }

useCounter.test.ts (Vitest ํ™˜๊ฒฝ ์˜ˆ์‹œ)

import { renderHook, act } from '@testing-library/react'; import { describe, it, expect } from 'vitest'; import { useCounter } from './useCounter'; describe('useCounter', () => { it('์ดˆ๊ธฐ๊ฐ’์œผ๋กœ 0์„ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•ด์š”', () => { const { result } = renderHook(() => useCounter()); expect(result.current.count).toBe(0); }); it('์ดˆ๊ธฐ๊ฐ’์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•ด์š”', () => { const { result } = renderHook(() => useCounter(10)); expect(result.current.count).toBe(10); }); it('increment ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด ์นด์šดํŠธ๊ฐ€ ์ฆ๊ฐ€ํ•ด์•ผ ํ•ด์š”', () => { const { result } = renderHook(() => useCounter(0)); act(() => { result.current.increment(); }); expect(result.current.count).toBe(1); }); it('decrement ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด ์นด์šดํŠธ๊ฐ€ ๊ฐ์†Œํ•ด์•ผ ํ•ด์š”', () => { const { result } = renderHook(() => useCounter(5)); act(() => { result.current.decrement(); }); expect(result.current.count).toBe(4); }); it('reset ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด ์ดˆ๊ธฐ๊ฐ’์œผ๋กœ ๋Œ์•„๊ฐ€์•ผ ํ•ด์š”', () => { const { result } = renderHook(() => useCounter(10)); act(() => { result.current.increment(); // 11 result.current.increment(); // 12 result.current.reset(); }); expect(result.current.count).toBe(10); }); it('์ƒˆ๋กœ์šด ์ดˆ๊ธฐ๊ฐ’์œผ๋กœ ๋ฆฌ์…‹๋˜์–ด์•ผ ํ•ด์š”', () => { const { result, rerender } = renderHook(({ initialValue }) => useCounter(initialValue), { initialProps: { initialValue: 0 }, }); act(() => { result.current.increment(); // count: 1 }); expect(result.current.count).toBe(1); rerender({ initialValue: 100 }); // initialValue ๋ณ€๊ฒฝ act(() => { result.current.reset(); // ์ƒˆ๋กœ์šด initialValue๋กœ ๋ฆฌ์…‹ }); expect(result.current.count).toBe(100); }); });

ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์—์„œ renderHook์„ ์‚ฌ์šฉํ•˜์—ฌ useCounter ํ›…์„ ๋ Œ๋”๋งํ•˜๊ณ , result.current๋ฅผ ํ†ตํ•ด ํ›…์˜ ๋ฐ˜ํ™˜ ๊ฐ’์— ์ ‘๊ทผํ•˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์–ด์š”. act ํ•จ์ˆ˜๋Š” ํ›… ๋‚ด๋ถ€์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์ƒํƒœ ์—…๋ฐ์ดํŠธ๊ฐ€ React์˜ ๋ Œ๋”๋ง ์‚ฌ์ดํด์— ๋งž์ถฐ ์ฒ˜๋ฆฌ๋˜๋„๋ก ๋ณด์žฅํ•ด ์ค€๋‹ต๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ํ›…์˜ ๋™์ž‘์„ ์•ˆ์ •์ ์œผ๋กœ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ์–ด์š”.

๐ŸŽฏ ์ปค์Šคํ…€ ํ›… ์‚ฌ์šฉ ์‹œ ์ฃผ์˜์‚ฌํ•ญ๊ณผ ํŒ

์ปค์Šคํ…€ ํ›…์€ ๊ฐ•๋ ฅํ•˜์ง€๋งŒ, ์ž˜๋ชป ์‚ฌ์šฉํ•˜๋ฉด ์˜คํžˆ๋ ค ์ฝ”๋“œ ๋ฒ ์ด์Šค๋ฅผ ๋ณต์žกํ•˜๊ฒŒ ๋งŒ๋“ค๊ฑฐ๋‚˜ ์„ฑ๋Šฅ ๋ฌธ์ œ๋ฅผ ์•ผ๊ธฐํ•  ์ˆ˜ ์žˆ์–ด์š”. ๋ช‡ ๊ฐ€์ง€ ์ฃผ์˜์‚ฌํ•ญ๊ณผ ํŒ์„ ์•Œ๋ ค๋“œ๋ฆด๊ฒŒ์š”.

0๏ธโƒฃ ๊ณผ๋„ํ•œ ์ถ”์ƒํ™” ํ”ผํ•˜๊ธฐ

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

1๏ธโƒฃ ํ›…์˜ ์ด๋ฆ„ ์ปจ๋ฒค์…˜

์ปค์Šคํ…€ ํ›…์€ ํ•ญ์ƒ use๋กœ ์‹œ์ž‘ํ•˜๋Š” ์ด๋ฆ„์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ด์š” (์˜ˆ: useToggle, useForm). ์ด๋Š” React์˜ ํ›… ๊ทœ์น™์ด๋ฉฐ, ๊ฐœ๋ฐœ์ž๋“ค์ด ํ•ด๋‹น ํ•จ์ˆ˜๊ฐ€ ํ›…์ž„์„ ์‰ฝ๊ฒŒ ์ธ์ง€ํ•˜๊ณ  ํ›… ๊ทœ์น™(์˜ˆ: ์ปดํฌ๋„ŒํŠธ ์ตœ์ƒ์œ„ ๋˜๋Š” ๋‹ค๋ฅธ ํ›… ๋‚ด๋ถ€์—์„œ๋งŒ ํ˜ธ์ถœ)์„ ๋”ฐ๋ฅด๋„๋ก ์œ ๋„ํ•˜๋Š” ์ค‘์š”ํ•œ ์ปจ๋ฒค์…˜์ด์—์š”.

2๏ธโƒฃ ํ›… ๋‚ด๋ถ€์—์„œ ๋‹ค๋ฅธ ํ›… ํ˜ธ์ถœ

์ปค์Šคํ…€ ํ›…์€ useState, useEffect์™€ ๊ฐ™์€ ๊ธฐ๋ณธ ํ›…๋ฟ๋งŒ ์•„๋‹ˆ๋ผ, ๋‹ค๋ฅธ ์ปค์Šคํ…€ ํ›…์„ ํ˜ธ์ถœํ•  ์ˆ˜๋„ ์žˆ์–ด์š”. ์ด๋Š” ๋” ๋ณต์žกํ•œ ๋กœ์ง์„ ์—ฌ๋Ÿฌ ๊ฐœ์˜ ์ž‘์€ ํ›…์œผ๋กœ ์ชผ๊ฐœ์–ด ๊ด€๋ฆฌํ•  ๋•Œ ์œ ์šฉํ•ด์š”. ์˜ˆ๋ฅผ ๋“ค์–ด, useForm ํ›… ๋‚ด๋ถ€์—์„œ useValidation ํ›…์„ ํ˜ธ์ถœํ•˜๋Š” ์‹์œผ๋กœ์š”. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ํ›… ๊ฐ„์˜ ๊ด€์‹ฌ์‚ฌ ๋ถ„๋ฆฌ๋ฅผ ๋”์šฑ ํšจ๊ณผ์ ์œผ๋กœ ํ•  ์ˆ˜ ์žˆ๋‹ต๋‹ˆ๋‹ค.

๐Ÿ“ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ

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

๐Ÿ“ฎ ์ฐธ๊ณ 

์—ฐ๊ด€๋œ ํฌ์ŠคํŠธ