๐Ÿš€ ๋‹จ์ˆœํ•œ UI๋„ ํ™•์žฅ ๊ฐ€๋Šฅํ•˜๊ฒŒ: Compound Component ํŒจํ„ด ์‚ฌ์šฉ ํ›„๊ธฐ

ํ˜œํ˜œยท2025๋…„ 5์›” 22์ผ
7

React

๋ชฉ๋ก ๋ณด๊ธฐ
10/10

๐Ÿ”น ์„œ๋ก : ๊ธ€์„ ์ž‘์„ฑํ•œ ์ด์œ 

์ตœ๊ทผ ํ”„๋กœ์ ํŠธ์—์„œ "์‚ฌ์šฉ์ž ์•ก์…˜์— ๋Œ€ํ•œ ๊ฒฐ๊ณผ๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ํŽ˜์ด์ง€"๋ฅผ ๊ตฌํ˜„ํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค! ์ •๋ง ๊ฐ„๋‹จํ•œ UI์˜€์ง€๋งŒ, ์ดˆ๊ธฐ๋ถ€ํ„ฐ ์—ฌ๋Ÿฌ ๊ฐ€์ง€ ๊ฒƒ๋“ค์„ ๊ณ ๋ คํ•ด ๋งŒ๋“ค์–ด ๋ณด๋ฉด ์ข‹์ง€ ์•Š์„๊นŒ ์‹ถ์–ด์„œ ์ปดํฌ๋„ŒํŠธํ™”๋ฅผ ์ง„ํ–‰ํ•˜๊ธฐ๋กœ ํ–ˆ๋‹ค. ์ด๋ฒˆ ๊ธ€์—์„œ๋Š” ๊ฒฐ๋ก ์ ์œผ๋กœ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์–ด๋–ป๊ฒŒ ๊ตฌ์„ฑํ–ˆ๊ณ , ๊ทธ ๊ณผ์ •์—์„œ ๋ฐฐ์šด ์ ์„ ์ ์–ด๋ณด๋ ค๊ณ  ํ•œ๋‹ค ๐Ÿ˜€


โœ… ๋ ˆ์ด์•„์›ƒ์€ ์ผ์ •ํ•˜๊ณ  ์•„์ด์ฝ˜, ์ œ๋ชฉ, ๋””์Šคํฌ๋ฆฝ์…˜๋งŒ ๋‹ฌ๋ผ์ง€๋Š” ๊ตฌ์กฐ
โœ… ํ–ฅํ›„ UI ๋ณ€๊ฒฝ ๊ฐ€๋Šฅ์„ฑ (ex. '๋ฉ”์ธ์œผ๋กœ ์ด๋™ํ•˜๊ธฐ' ๋ฒ„ํŠผ ์ถ”๊ฐ€ ๋“ฑ)
โœ… ํ™”๋ฉด ๊ตฌ์„ฑ ์ค‘ ์ผ๋ถ€๋งŒ ์“ฐ์ด๋Š” ๊ฒฝ์šฐ (ex. ๋””์Šคํฌ๋ฆฝ์…˜ ์—†์ด ์•„์ด์ฝ˜๊ณผ ์ œ๋ชฉ๋งŒ ์“ฐ์ด๋Š” ๊ฒฝ์šฐ)

์œ„์™€ ๊ฐ™์€ ์š”๊ตฌ์‚ฌํ•ญ ๋ฐ ๊ฐ€๋Šฅ์„ฑ์„ ๊ณ ๋ คํ–ˆ์„ ๋•Œ, ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํŠน์ง•์ด ํ•„์š”ํ•˜๋‹ค๋Š” ๊ฑธ ์•Œ ์ˆ˜ ์žˆ๊ฒ ๋‹ค.

โญ•๏ธ ๋ ˆ์ด์•„์›ƒ์€ ๊ณตํ†ต + ๋‚ด์šฉ๋งŒ ๋‹ฌ๋ผ์ง€๋Š” ๊ตฌ์กฐ ํ•„์š”
โญ•๏ธ ๋ณ€๊ฒฝ์— ์šฉ์ดํ•œ ๊ตฌ์กฐ ํ•„์š”
โญ•๏ธ ๊ฐ ์š”์†Œ๊ฐ€ ์„œ๋กœ ์˜์กด์„ฑ์ด ์ ์€ ๊ตฌ์กฐ ํ•„์š”

๊ฒฐ๋ก ์ ์œผ๋กœ ๋‚˜๋Š” ์ด๋Ÿฐ ์กฐ๊ฑด์„ ์ถฉ์กฑํ•˜๋Š” ๊ฐ€์žฅ ํšจ๊ณผ์ ์ธ ํŒจํ„ด์ด ๐Ÿ’ก ํ•ฉ์„ฑ ์ปดํฌ๋„ŒํŠธ๋ผ๊ณ  ์ƒ๊ฐํ–ˆ๊ณ , ํ•ด๋‹น ํŒจํ„ด์„ ์ด์šฉํ•ด์„œ UI๋ฅผ ๊ฐœ๋ฐœํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค.


๐Ÿ”น ํ•ฉ์„ฑ ์ปดํฌ๋„ŒํŠธ๋ž€?

ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์–ด๋–ป๊ฒŒ ๊ตฌ์„ฑํ• ์ง€ ๊ณ ๋ฏผํ•˜๋‹ค ๋ณด๋ฉด, "์–ด๋–ป๊ฒŒ ํ•˜๋ฉด ๋” ์œ ์—ฐํ•˜๊ณ  ์žฌ์‚ฌ์šฉํ•˜๊ธฐ ์‰ฌ์šด ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์„๊นŒ?"๋ผ๋Š” ์งˆ๋ฌธ์„ ์ž์ฃผ ํ•˜๊ฒŒ ๋œ๋‹ค.
๊ทธ ๊ณ ๋ฏผ์˜ ํ•ด๊ฒฐ์ฑ… ์ค‘ ํ•˜๋‚˜๊ฐ€ ๋ฐ”๋กœ ํ•ฉ์„ฑ(Composition)์ด๋‹ค!

๐Ÿ”ธ Composition vs Inheritance

React๋Š” ์ „ํ†ต์ ์ธ ํด๋ž˜์Šค ๊ธฐ๋ฐ˜ ์–ธ์–ด์—์„œ ๋งŽ์ด ์“ฐ์ด๋Š” ์ƒ์†(Inheritance)๋ณด๋‹ค๋Š”, ํ•ฉ์„ฑ(Composition)์„ ๋” ์„ ํ˜ธํ•œ๋‹ค. ํ•ฉ์„ฑ์€ "์—ฌ๋Ÿฌ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์กฐํ•ฉํ•ด์„œ ์ƒˆ๋กœ์šด UI๋ฅผ ๋งŒ๋“ค์–ด๋‚ด๋Š” ๋ฐฉ์‹"์œผ๋กœ, ์ปดํฌ๋„ŒํŠธ ๊ฐ„์˜ ๊ฒฐํ•ฉ๋„๋ฅผ ๋‚ฎ์ถ”๊ณ  ๋” ์œ ์—ฐํ•œ ๊ตฌ์กฐ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค€๋‹ค.

  • ์ƒ์† : ๊ธฐ๋Šฅ์„ ์ƒ์†๋ฐ›์•„ ํ™•์žฅํ•˜๋Š” ๋ฐฉ์‹. ์ปดํฌ๋„ŒํŠธ ๊ฐ„ ๊ฐ•ํ•œ ์˜์กด์ด ์ƒ๊ธฐ๊ณ , ์žฌ์‚ฌ์šฉ์ด ์–ด๋ ต๋‹ค.
  • ํ•ฉ์„ฑ : ์—ฌ๋Ÿฌ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์กฐํ•ฉํ•ด ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ์„ ๊ตฌ์„ฑํ•˜๋Š” ๋ฐฉ์‹. ๋…๋ฆฝ์„ฑ๊ณผ ํ™•์žฅ์„ฑ์ด ์ข‹๋‹ค.

React ๊ณต์‹ ๋ฌธ์„œ์—์„œ๋„ ์•„๋ž˜์™€ ๊ฐ™์ด ๋งํ•˜๊ณ  ์žˆ๋‹ค ๐Ÿ‘€

"React๋Š” ๊ฐ•๋ ฅํ•œ ํ•ฉ์„ฑ ๋ชจ๋ธ์„ ๊ฐ€์ง€๊ณ  ์žˆ์œผ๋ฉฐ, ์ƒ์† ๋Œ€์‹  ํ•ฉ์„ฑ์„ ์‚ฌ์šฉํ•˜์—ฌ ์ปดํฌ๋„ŒํŠธ ๊ฐ„์— ์ฝ”๋“œ๋ฅผ ์žฌ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค."

"Facebook์—์„œ๋Š” ์ˆ˜์ฒœ ๊ฐœ์˜ React ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ฌ์šฉํ•˜์ง€๋งŒ, ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ƒ์† ๊ณ„์ธต ๊ตฌ์กฐ๋กœ ์ž‘์„ฑ์„ ๊ถŒ์žฅํ• ๋งŒํ•œ ์‚ฌ๋ก€๋ฅผ ์•„์ง ์ฐพ์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค."


๐Ÿ”ธ ์˜ˆ์‹œ: children ๊ธฐ๋ฐ˜ ํ•ฉ์„ฑ

๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ ํ•ฉ์„ฑ ๋ฐฉ์‹์€ children์„ ํ†ตํ•ด ํ•˜์œ„ ์š”์†Œ๋ฅผ ์ฃผ์ž…๋ฐ›๋Š” ๋ฐฉ์‹์ด๋‹ค.

function Card({ children }: { children: React.ReactNode }) {
  return <div className="p-4 border rounded-md">{children}</div>;
}

<Card>
  <h2>๊ณต์ง€์‚ฌํ•ญ</h2>
  <p>์˜ค๋Š˜์€ ์˜คํ›„ 6์‹œ์— ์ ๊ฒ€์ด ์žˆ์Šต๋‹ˆ๋‹ค.</p>
</Card>

์ด ์˜ˆ์ œ์—์„œ Card๋Š” ๋‚ด๋ถ€ ๊ตฌ์กฐ๋ฅผ ๊ณ ์ •ํ•˜์ง€ ์•Š๊ณ , ๋ถ€๋ชจ๊ฐ€ ๋„˜๊ธด ๋‚ด์šฉ์„ ๊ทธ๋Œ€๋กœ ๊ฐ์‹ธ์„œ ๋ณด์—ฌ์ค€๋‹ค. ์ด๋Ÿฐ ๋ฐฉ์‹์€ ๋ ˆ์ด์•„์›ƒ, ๋ชจ๋‹ฌ, ํˆดํŒ ๋“ฑ ๋‹ค์–‘ํ•œ UI ์š”์†Œ์— ์“ธ ์ˆ˜ ์žˆ๋‹ค.


๐Ÿ”ธ React์—์„œ ์ž์ฃผ ์“ฐ์ด๋Š” ํ•ฉ์„ฑ ํŒจํ„ด

React์—์„œ๋Š” ํ•ฉ์„ฑ์„ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ์‹์ด ๊ฝค ๋‹ค์–‘ํ•˜๋‹ค. ๊ทธ์ค‘ ์ž์ฃผ ์“ฐ์ด๋Š” ๋Œ€ํ‘œ์ ์ธ ํŒจํ„ด์€ ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

1๏ธโƒฃ Slot ํŒจํ„ด
์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์˜ ํŠน์ • ์˜์—ญ์— ๋‚ด์šฉ์„ ๋ช…ํ™•ํ•˜๊ฒŒ ์ฃผ์ž…ํ•˜๊ณ  ์‹ถ์„ ๋•Œ ์“ฐ๋Š” ๋ฐฉ์‹์ด๋‹ค.
์˜ˆ๋ฅผ ๋“ค์–ด ์ œ๋ชฉ, ๋ณธ๋ฌธ, ํ‘ธํ„ฐ๋ฅผ ๊ฐ๊ฐ ๋‹ค๋ฅธ ์Šฌ๋กฏ์œผ๋กœ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.

function Dialog({ title, body, footer }: Props) {
  return (
    <div className="dialog">
      <div className="dialog-title">{title}</div>
      <div className="dialog-body">{body}</div>
      <div className="dialog-footer">{footer}</div>
    </div>
  );
}

<Dialog
  title={<h2>ํšŒ์›๊ฐ€์ž…</h2>}
  body={<SignUpForm />}
  footer={<Button>ํ™•์ธ</Button>}
/>

2๏ธโƒฃ Compound Component ํŒจํ„ด
ํ•˜์œ„ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋…ผ๋ฆฌ์ ์œผ๋กœ ๋ฌถ์–ด์„œ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ์‹์ด๋‹ค.
๋ณดํ†ต Tabs, Accordion, Select ๊ฐ™์€ ๊ตฌ์กฐ์—์„œ ๋งŽ์ด ์“ด๋‹ค.

const Tabs = ({ children }) => <div>{children}</div>;
Tabs.TabList = ({ children }) => <div className="tab-list">{children}</div>;
Tabs.TabPanel = ({ children }) => <div className="tab-panel">{children}</div>;

<Tabs>
  <Tabs.TabList>
    <button>ํƒญ 1</button>
    <button>ํƒญ 2</button>
  </Tabs.TabList>
  <Tabs.TabPanel>
    <p>ํƒญ 1์˜ ๋‚ด์šฉ</p>
  </Tabs.TabPanel>
</Tabs>

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

๋‘ ํŒจํ„ด์„ ํ‘œ๋กœ ๋น„๊ตํ•˜์ž๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

ํ•ญ๋ชฉSlot ํŒจํ„ดCompound Component ํŒจํ„ด
๊ตฌ์„ฑ ๋ฐฉ์‹๋ช…์‹œ์ ์ธ prop์œผ๋กœ ์˜์—ญ๋ณ„ ๋‚ด์šฉ์„ ์ „๋‹ฌํ•จํ•˜์œ„ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋…ผ๋ฆฌ์ ์œผ๋กœ ์กฐํ•ฉํ•˜์—ฌ ๊ตฌ์„ฑ
์ปดํฌ๋„ŒํŠธ ๊ตฌ์กฐ ์ œ์–ด ๋ฐฉ์‹์ƒ์œ„ ์ปดํฌ๋„ŒํŠธ์—์„œ ๊ตฌ์กฐ ์ œ์–ด์ƒ์œ„ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ํ•˜์œ„ ์ปดํฌ๋„ŒํŠธ ์ง‘ํ•ฉ์„ ์ œ๊ณต
์œ ์—ฐ์„ฑprop ๊ตฌ์กฐ๋ฅผ ๋”ฐ๋ผ์•ผ ํ•˜๋ฏ€๋กœ ๋‹ค์†Œ ๊ณ ์ •์ children ์กฐํ•ฉ์œผ๋กœ ์œ ์—ฐํ•œ ๊ตฌ์„ฑ ๊ฐ€๋Šฅ
ํ™•์žฅ์„ฑ์ƒˆ๋กœ์šด ์Šฌ๋กฏ์„ ์ถ”๊ฐ€ํ•˜๋ ค๋ฉด props ์ˆ˜์ • ํ•„์š”์ปดํฌ๋„ŒํŠธ๋งŒ ์ถ”๊ฐ€ํ•˜๋ฉด ๋˜๋ฏ€๋กœ ํ™•์žฅ์— ์œ ๋ฆฌํ•จ
์‹ค์ œ ์‚ฌ์šฉ ์˜ˆDialog, Modal, Card ๋“ฑ ๊ตฌ์„ฑ ์š”์†Œ๊ฐ€ ๋ช…ํ™•ํžˆ ๊ตฌ๋ถ„๋˜๋Š” ์ปดํฌ๋„ŒํŠธTabs, Accordion, Select, Menu ๋“ฑ ๋ณต์žกํ•œ ๊ตฌ์„ฑ์˜ UI ์ปดํฌ๋„ŒํŠธ

๊ทธ๋Ÿฌ๋ฏ€๋กœ ๋‚˜๋Š” ์•ž์„œ ์–˜๊ธฐํ–ˆ๋˜ "๋ณ€๊ฒฝ์— ์šฉ์ดํ•œ ๊ตฌ์กฐ", "์„œ๋กœ ์˜์กด์„ฑ์ด ์ ์€ ๊ตฌ์กฐ"๋ผ๋Š” ์กฐ๊ฑด์„ ์ถฉ์กฑํ•˜๋ ค๋ฉด, Slot ํŒจํ„ด๋ณด๋‹ค๋Š” Compound Component ํŒจํ„ด์ด ๋ณด๋‹ค ์ ํ•ฉํ•  ๊ฑฐ๋ผ๊ณ  ํŒ๋‹จํ–ˆ๊ณ , ํ•ด๋‹น ํŒจํ„ด์— ๋งž์ถฐ ๊ตฌํ˜„์„ ์ง„ํ–‰ํ•˜๊ธฐ๋กœ ํ–ˆ๋‹ค ๐Ÿ˜€


๐Ÿ”น Compound Component ํŒจํ„ด์œผ๋กœ ๊ตฌํ˜„

๐Ÿ”ธ ๊ตฌ์กฐํ™”

๊ตฌ์กฐํ™”๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ง„ํ–‰ํ–ˆ๋‹ค. ์—ฌ๊ธฐ์— ์ถ”๊ฐ€๋กœ Icon์€ ์ž์œ ๋„๊ฐ€ ๋†’๊ธฐ๋ณด๋‹ค๋Š”, ์ƒํ™ฉ์— ๋งž๋Š” ์•„์ด์ฝ˜์ด ๋ช…์‹œ์ ์œผ๋กœ ์‚ฌ์šฉ๋  ๊ฐ€๋Šฅ์„ฑ์ด ๋” ๋†’๊ธฐ ๋•Œ๋ฌธ์—, Result.Image ๋ถ€๋ถ„์—์„œ๋Š” type์„ ์ข€ ๋” ํ™œ์šฉํ•ด ๋ณด๊ธฐ๋กœ ํ–ˆ๋‹ค.


์ตœ์ข… ์ฝ”๋“œ๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

๐Ÿ”ธ ๊ตฌํ˜„๋ถ€

import Image from "next/image";

import { RESULT_IMG_MAP } from "@/constants/result/resultImg";

export type ImgKind = "letter";

function ResultImage({ kind }: { kind: ImgKind }) {
  return (
    <Image
      src={RESULT_IMG_MAP[kind]}
      alt="๊ฒฐ๊ณผ ์ด๋ฏธ์ง€"
      width={80}
      height={80}
    />
  );
}

function ResultTitle({ children }: { children: React.ReactNode }) {
  return <h1 className="mt-6 text-2xl font-bold text-gray-900">{children}</h1>;
}

function ResultDescription({ children }: { children: React.ReactNode }) {
  return (
    <p className="mt-2 whitespace-pre font-medium text-gray-400">{children}</p>
  );
}

function ResultMain({ children }: { children: React.ReactNode }) {
  return (
    <div className="flex flex-col items-center justify-center text-center">
      {children}
    </div>
  );
}

export const Result = Object.assign(ResultMain, {
  Image: ResultImage,
  Title: ResultTitle,
  Description: ResultDescription,
});

๊ตฌํ˜„๋ถ€์—์„œ๋Š” ๊ฐ ํ•˜์œ„ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค์–ด์ฃผ๊ณ , Object.assign()์„ ์ด์šฉํ•ด ์ปดํฌ๋„ŒํŠธ๋ฅผ ์กฐํ•ฉํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„๋œ Result ์ปดํฌ๋„ŒํŠธ์ด๋‹ค. Object.assign()์€ "์ถœ์ฒ˜ ๊ฐ์ฒด๋“ค์˜ ๋ชจ๋“  ์—ด๊ฑฐ ๊ฐ€๋Šฅํ•œ ์†์„ฑ์„ ๋ณต์‚ฌํ•ด ๋Œ€์ƒ ๊ฐ์ฒด์— ๋ถ™์—ฌ๋„ฃ๋Š”" ๋ฉ”์†Œ๋“œ์ธ๋ฐ, ์ด ๊ณผ์ •์ด ํ•„์š”ํ•œ ์ด์œ ๋Š” ํ•ฉ์„ฑ ์ปดํฌ๋„ŒํŠธ ํŒจํ„ด์œผ๋กœ ์‚ฌ์šฉํ•˜๋ ค๋ฉด "ํ•˜์œ„ ์ปดํฌ๋„ŒํŠธ๋“ค์„ ์ƒ์œ„ ์ปดํฌ๋„ŒํŠธ์— ์†์„ฑ์œผ๋กœ ๋ถ™์—ฌ์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ"์ด๋‹ค. ReactMain์— ํ•˜์œ„ ์ปดํฌ๋„ŒํŠธ๋“ค์„ ๋ถ™์—ฌ ํ•ฉ์„ฑ ์ปดํฌ๋„ŒํŠธ๋กœ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด ๋™์ ์œผ๋กœ ์†์„ฑ์„ ๋ถ€์—ฌํ•˜๋Š” ๊ณผ์ •์ด๋‹ค.

TypeScript์—์„œ๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด ๋ช…์‹œ์ ์œผ๋กœ ์ง€์ •ํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

type ResultComponent = typeof ResultMain & {
  Image: typeof ResultImage;
  Title: typeof ResultTitle;
  Description: typeof ResultDescription;
};

const Result = ResultMain as ResultComponent;
Result.Image = ResultImage;
Result.Title = ResultTitle;
Result.Description = ResultDescription;

Object.assign() ๋ฐฉ์‹์ด ์ข€ ๋” ์ฝ”๋“œ๊ฐ€ ์งง์•„์ง€๊ณ  ์ง๊ด€์ ์ด๋ผ ํ•ด๋‹น ๋ฉ”์†Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฑธ ๊ถŒ์žฅํ•œ๋‹ท...!!

+) ์•„์ฃผ ์ค‘์š”ํ•œ ๋ถ€๋ถ„์€ ์•„๋‹ˆ์ง€๋งŒ ์ฐธ๊ณ ๋กœ RESULT_IMG_MAP์€ ์•„์ด์ฝ˜ ์ข…๋ฅ˜๋ณ„๋กœ ์–ด๋–ค ์ด๋ฏธ์ง€๋ฅผ ์“ธ ๊ฑด์ง€ ์ง€์ •ํ•ด๋†“์€ ์ƒ์ˆ˜์ด๋‹ค.


๐Ÿ”ธ ์‚ฌ์šฉ๋ถ€

import { Result } from "@/ui/Result";

function NotifyResultPage() {
  return (
    <div className="flex h-screen w-full items-center justify-center">
      <Result>
        <Result.Image kind="letter" />
        <Result.Title>๋งค์นญ ์‹ ์ฒญ์ด ์™„๋ฃŒ๋์–ด์š”!</Result.Title>
        <Result.Description>
          {`๋งค์นญ์ด ์„ฑ์‚ฌ๋˜๋ฉด\n์นดํ†ก์œผ๋กœ ์•Œ๋ ค๋“œ๋ฆด๊ฒŒ์š”`}
        </Result.Description>
      </Result>
    </div>
  );
}

export default NotifyResultPage;

์‚ฌ์šฉ๋ถ€์—์„œ๋Š” ์ด๋Ÿฐ ์‹์œผ๋กœ ์กฐํ•ฉํ•ด์„œ ์‚ฌ์šฉํ•ด์ฃผ๋ฉด ๋œ๋‹ค! ๐Ÿ‘


๐Ÿ”ธ Compound Component ํŒจํ„ด์œผ๋กœ ๊ตฌํ˜„ํ•˜์ง€ ์•Š์•˜๋‹ค๋ฉด?

import Image from "next/image";
import { RESULT_IMG_MAP } from "@/constants/result/resultImg";

export type ImgKind = "letter";

interface ResultProps {
  kind: ImgKind;
  title: React.ReactNode;
  description: React.ReactNode;
}

export function Result({ kind, title, description }: ResultProps) {
  return (
    <div className="flex flex-col items-center justify-center text-center">
      <Image
        src={RESULT_IMG_MAP[kind]}
        alt="์™„๋ฃŒ ์ด๋ฏธ์ง€"
        width={80}
        height={80}
      />
      <h1 className="mt-6 text-2xl font-bold text-gray-900">{title}</h1>
      <p className="mt-2 whitespace-pre font-medium text-gray-400">{description}</p>
    </div>
  );
}

์ด๋Ÿฌํ•œ ๋ฐฉ์‹์€ ์ฝ”๋“œ๊ฐ€ ๋” ์งง์•„์งˆ ์ˆœ ์žˆ๊ฒ ์ง€๋งŒ, ๋ณด๋‹ค ๋น„์ง๊ด€์ ์ด๊ณ  ์ปจํ…์ธ ์˜ ์œ„์น˜๊ฐ€ ๊ณ ์ •์ ์ด๋ผ์„œ ์œ ์—ฐ์„ฑ์ด ๋–จ์–ด์ง„๋‹ค๋Š” ๋‹จ์ ์ด ์žˆ์„ ๊ฒƒ ๊ฐ™๋‹ค.


๐Ÿ”น ํ•ฉ์„ฑ ์ปดํฌ๋„ŒํŠธ, ํ•ญ์ƒ ์ข‹์„๊นŒ?

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

๐Ÿ”ธ ๋‹จ์ˆœํ•œ UI์—์„œ๋Š” ์˜คํžˆ๋ ค ๊ณผ์„ค๊ณ„์ผ ์ˆ˜ ์žˆ๋‹ค

์˜ˆ๋ฅผ ๋“ค์–ด ๋‹จ์ˆœํ•œ ๋ฒ„ํŠผ์ด๋‚˜ ๋ผ๋ฒจ, ๋ ˆ์ด์•„์›ƒ์ด ๊ฑฐ์˜ ๊ณ ์ •๋œ ํŒ์—… ๋“ฑ์€ children์ด๋‚˜ ์—ฌ๋Ÿฌ ํ•˜์œ„ ์ปดํฌ๋„ŒํŠธ๋ฅผ ํ†ตํ•œ ํ•ฉ์„ฑ ๊ตฌ์กฐ๋ฅผ ๋งŒ๋“ค๊ธฐ๋ณด๋‹ค๋Š” ๋‹จ์ˆœํ•œ prop ๊ธฐ๋ฐ˜ ์ปดํฌ๋„ŒํŠธ๋กœ ์„ค๊ณ„ํ•˜๋Š” ๊ฒƒ์ด ๋” ๋‚˜์€ ์„ ํƒ์ง€๊ฐ€ ๋  ์ˆ˜ ์žˆ๋‹ค.

โŒ ์•ˆ ์ข‹์€ ์˜ˆ์‹œ

<Button.Root>
  <Button.Label>ํ™•์ธ</Button.Label>
  <Button.Icon src="check.svg" />
</Button.Root>

โ†’ ์‹ค์ œ๋กœ๋Š” ๊ทธ๋ƒฅ <Button icon="check.svg">ํ™•์ธ</Button>์œผ๋กœ๋„ ์ถฉ๋ถ„ํ•œ ๊ฒฝ์šฐ๊ฐ€ ๋งŽ๋‹ค.

๋ถˆํ•„์š”ํ•œ ํ•ฉ์„ฑ ๊ตฌ์กฐ๋Š” ์ฝ”๋“œ๋Ÿ‰์„ ๋Š˜๋ฆฌ๊ณ , ํŒ€์›๋“ค์ด ํ•ด๋‹น ๊ตฌ์กฐ๋ฅผ ํ•™์Šตํ•ด์•ผ ํ•˜๋Š” ๋ถ€๋‹ด๋„ ํ•จ๊ป˜ ์ƒ๊ธด๋‹ค ๐Ÿ‘€


๐Ÿ”ธ ๊ทธ๋Ÿผ ์–ด๋–จ ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ๊ฒŒ ์ข‹์„๊นŒ?

1. ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋‹ค์–‘ํ•œ ๋‚ด๋ถ€ ๊ตฌ์กฐ๋ฅผ ๊ฐ€์งˆ ์ˆ˜ ์žˆ์„ ๋•Œ

ex) Dialog, Form, Card์ฒ˜๋Ÿผ ๋ณธ๋ฌธ, ์ œ๋ชฉ, ํ‘ธํ„ฐ ๋“ฑ์„ ์œ ๋™์ ์œผ๋กœ ์กฐํ•ฉํ•˜๊ณ  ์‹ถ์€ ๊ฒฝ์šฐ
ex) ์ปดํฌ๋„ŒํŠธ ๊ตฌ์„ฑ ์ค‘ ์ผ๋ถ€๋งŒ ์“ฐ์ด๋Š” ๊ฒฝ์šฐ๋„ ์กด์žฌํ•  ๋•Œ

2. ํ˜‘์—…ํ•˜๋Š” ๊ฐœ๋ฐœ์ž์—๊ฒŒ ๋ช…ํ™•ํ•œ ๊ตฌ์กฐ๋ฅผ ์ œ์•ˆํ•˜๊ณ  ์‹ถ์„ ๋•Œ

ex) Tabs.TabList, Tabs.TabPanel์ฒ˜๋Ÿผ ์‚ฌ์šฉ์ž๊ฐ€ ์ง๊ด€์ ์œผ๋กœ ์–ด๋–ค ์ปดํฌ๋„ŒํŠธ๋ฅผ ์จ์•ผ ํ•˜๋Š”์ง€ ์•Œ ์ˆ˜ ์žˆ์Œ

3. ๋ณต์žกํ•œ UI ๋กœ์ง์„ Context๋ฅผ ํ†ตํ•ด ๊ณต์œ ํ•  ํ•„์š”๊ฐ€ ์žˆ์„ ๋•Œ

ex) ํƒญ ์ „ํ™˜, ์„ ํƒ ์ƒํƒœ ๋“ฑ ์—ฌ๋Ÿฌ ํ•˜์œ„ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋™์ผํ•œ ์ƒํƒœ๋ฅผ ๊ณต์œ ํ•  ๊ฒฝ์šฐ

์ฆ‰, ํ•ฉ์„ฑ ์ปดํฌ๋„ŒํŠธ๋Š” (1) UI ๊ตฌ์กฐ์˜ ์œ ์—ฐ์„ฑ์ด ํ•„์š”ํ•˜๊ณ  (2) ํ•˜์œ„ ์ปดํฌ๋„ŒํŠธ๋“ค์ด ๋…ผ๋ฆฌ์ ์œผ๋กœ ์—ฐ๊ฒฐ๋˜์–ด ์žˆ์„ ๋•Œ(๋˜๋Š” ์ƒํƒœ๋ฅผ ๊ณต์œ ํ•  ๋•Œ) ์œ ์šฉํ•œ ํŒจํ„ด์ด๋ผ๊ณ  ์ƒ๊ฐํ•œ๋‹ค!


๐Ÿ”น ๋งˆ๋ฌด๋ฆฌ

์˜ˆ์ „์— ์ธํ„ดํ•  ๋•Œ์—๋Š” ํ•ฉ์„ฑ ์ปดํฌ๋„ŒํŠธ๋กœ UI๋ฅผ ๊ตฌ์„ฑํ•ด ๋ณธ ์  ์žˆ๋А๋ƒ๋Š” ์‚ฌ์ˆ˜๋‹˜์˜ ์งˆ๋ฌธ์— ํ•ด ๋ณธ ์ ์ด ์—†๋‹ค๊ณ  ๋Œ€๋‹ตํ–ˆ์—ˆ๋Š”๋ฐ ์ด์ œ๋Š” ์žˆ๋‹ค๊ณ  ๋Œ€๋‹ตํ•  ์ˆ˜ ์žˆ๋Š” ์‚ฌ๋žŒ์ด ๋˜์—ˆ๋‹ค(ใ…Žใ…Ž) ๋„์ž… ์ „ํ›„๋กœ ํ•ฉ์„ฑ ์ปดํฌ๋„ŒํŠธ์— ๋Œ€ํ•ด ์ž์„ธํžˆ ๊ณต๋ถ€ํ•ด ๋ณด์•˜๋Š”๋ฐ, ๋‹จ์ˆœํ•˜๋ฉด์„œ๋„ ๊ฐ•๋ ฅํ•œ ํŒจํ„ด์ด๋ผ๋Š” ์ƒ๊ฐ์ด ๋“ ๋‹ค. ์ปดํฌ๋„ŒํŠธํ™”๋ฅผ ํ•  ๋•Œ ์•ž์œผ๋กœ ๊ณ ๋ คํ•  ์˜ต์…˜์ด ํ•˜๋‚˜ ๋” ๋Š˜์–ด๋‚œ ๊ฒƒ ๊ฐ™๋‹ค! ์žฌ์‚ฌ์šฉ์„ฑ ์ข‹์€ ์ปดํฌ๋„ŒํŠธ์— ๋Œ€ํ•ด ๋” ๊ณ ๋ฏผํ•˜๊ณ  ๋ฐœ์ „ํ•˜๋Š” ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž๊ฐ€ ๋˜์–ด์•ผ๊ฒ ๋‹ค ๐Ÿซ  ์ฝ์–ด์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค!

ํ”ผ๋“œ๋ฐฑ์€ ์–ธ์ œ๋‚˜ ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค ๐Ÿค—


๐Ÿ”น ์ฐธ๊ณ  ๋ฌธ์„œ

profile
์‰ฝ๊ฒŒ๋งŒ์‚ด์•„๊ฐ€๋ฉด์žฌ๋ฏธ์—†์–ด๋น™๊ณ 

6๊ฐœ์˜ ๋Œ“๊ธ€

comment-user-thumbnail
2025๋…„ 5์›” 22์ผ

์•ˆ๊ทธ๋ž˜๋„ ํ•ด๋‹น ์ฃผ์ œ๋กœ ํž˜๋“ค์–ดํ•˜๊ณ  ์žˆ์—ˆ๋Š”๋ฐ ์ž˜ ์ฝ์—ˆ์Šต๋‹ˆ๋‹ค!!

1๊ฐœ์˜ ๋‹ต๊ธ€
comment-user-thumbnail
2025๋…„ 5์›” 26์ผ

์‚ฌ์‹ค ์š”์ฆ˜์€ ai๊ฐ€ ํ•ด์ฃผ๊ธฐ๋•Œ๋ฌธ์— ๊ณผ์ŠคํŽ™์€์•„๋‹Œ๊ฒƒ ๊ฐ™์•„์š”. ์˜คํžˆ๋ ค aiํ•œํ…Œ Ui๋งก๊ธฐ๋ฉด ์•„๋ฌด๋ž˜๋„ ์žฌ์‚ฌ์šฉ์„ฑ๋ฉด์—์„œ ์•„์‰ฌ์šธ๋ฐ๋„ ๋งŽ์€๋ฐ compound๋ฅผํ†ตํ•ด ์ด๋Ÿฐ๋ฉด์ด ํ•ด๊ฒฐ๊ฐ€๋Šฅํ•œ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค

1๊ฐœ์˜ ๋‹ต๊ธ€
comment-user-thumbnail
2025๋…„ 6์›” 2์ผ

์—ด์‹ฌํžˆ ๋…ธ๋ ฅํ•˜๋Š” ํ˜œํ˜œ๋‹˜์˜ ๋ฏธ๋ž˜๋Š” ๋ฐ์•„์š”.. ์กฐ๊ธˆ๋งŒ ๋” ํž˜๋‚ด๋ด…์‹œ๋‹ค ์•„์ž์•„์ž

1๊ฐœ์˜ ๋‹ต๊ธ€