Skip to content

๐Ÿชต 6. ์ข‹์€ ์ปดํฌ๋„ŒํŠธ๋ž€ ๋ฌด์—‡์ธ๊ฐ€? Headless Pattern

Taeyeon Yoon edited this page Dec 5, 2024 · 1 revision

ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์—์„œ ๋งˆ์ฃผํ•˜๋Š” ํ•ต์‹ฌ ๊ณผ์ œ

ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์—์„œ๋Š” ๋‹ค์–‘ํ•œ ํ•ต์‹ฌ ๊ณผ์ œ๋“ค์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.

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

์ด๋Ÿฌํ•œ ๊ณผ์ œ๋“ค์„ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด Headless Pattern, Tailwind CSS, Storybook์„ ์ฑ„ํƒํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ํšจ๊ณผ์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋จผ์ € "์ข‹์€ ์ปดํฌ๋„ŒํŠธ"๊ฐ€ ๋ฌด์—‡์ธ์ง€ ์ดํ•ดํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค.

์ข‹์€ ์ปดํฌ๋„ŒํŠธ์˜ ์กฐ๊ฑด: ์‘์ง‘๋„์™€ ๊ฒฐํ•ฉ๋„

1. ๋†’์€ ์‘์ง‘๋„ (High Cohesion)

  • ๋‹จ์ผ ์ฑ…์ž„ ์›์น™: ์ปดํฌ๋„ŒํŠธ๋Š” ํ•˜๋‚˜์˜ ๋ช…ํ™•ํ•œ ์—ญํ• ์— ์ง‘์ค‘ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  • ์˜๋ฏธ ์žˆ๋Š” ๋„ค์ด๋ฐ: ์ปดํฌ๋„ŒํŠธ ์ด๋ฆ„์€ ๊ทธ ์—ญํ• ๊ณผ ๋ชฉ์ ์„ ๋ช…ํ™•ํžˆ ๋‚˜ํƒ€๋‚ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  • ๋ช…ํ™•ํ•œ ์ธํ„ฐํŽ˜์ด์Šค: ์ž˜ ์ •์˜๋œ props์™€ ์ด๋ฒคํŠธ
// ๋†’์€ ์‘์ง‘๋„๋ฅผ ๊ฐ€์ง„ ์ปดํฌ๋„ŒํŠธ ์˜ˆ์‹œ
interface SearchInputProps {
  value: string;
  onChange: (value: string) => void;
  onSubmit: () => void;
}

const SearchInput: React.FC<SearchInputProps> = ({ value, onChange, onSubmit }) => {
  // ๊ฒ€์ƒ‰ ์ž…๋ ฅ์— ๊ด€๋ จ๋œ ๋กœ์ง๋งŒ ํฌํ•จ
  return (
    <form onSubmit={onSubmit}>
      <input
        type="search"
        value={value}
        onChange={e => onChange(e.target.value)}
      />
    </form>
  );
};

2. ๋‚ฎ์€ ๊ฒฐํ•ฉ๋„ (Low Coupling)

  • ๋…๋ฆฝ์  ๋™์ž‘: ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ์— ์˜์กดํ•˜์ง€ ์•Š๊ณ  ๋…๋ฆฝ์ ์œผ๋กœ ๋™์ž‘ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  • ์œ ์—ฐํ•œ ํ•ฉ์„ฑ: ๋‹ค์–‘ํ•œ ์ƒํ™ฉ์—์„œ ์กฐํ•ฉํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•˜๋ฉฐ, ๋…๋ฆฝ์ ์ธ ์ˆ˜์ •๊ณผ ํ™•์žฅ์ด ๊ฐ€๋Šฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  • ์ œ์–ด์˜ ์—ญ์ „: ์ฃผ์š” ๊ฒฐ์ •๊ถŒ์„ ์™ธ๋ถ€๋กœ ์œ„์ž„ํ•˜์—ฌ ์žฌ์‚ฌ์šฉ์„ฑ์„ ๋†’์ž…๋‹ˆ๋‹ค.
// ๋‚ฎ์€ ๊ฒฐํ•ฉ๋„๋ฅผ ๊ฐ€์ง„ ์ปดํฌ๋„ŒํŠธ ์˜ˆ์‹œ
const Card: React.FC<{
  renderHeader?: () => React.ReactNode;
  renderFooter?: () => React.ReactNode;
  children: React.ReactNode;
}> = ({ renderHeader, renderFooter, children }) => {
  return (
    <div className="card">
      {renderHeader && <div className="card-header">{renderHeader()}</div>}
      <div className="card-body">{children}</div>
      {renderFooter && <div className="card-footer">{renderFooter()}</div>}
    </div>
  );
};

๋†’์€ ์‘์ง‘๋„์™€ ๋‚ฎ์€ ๊ฒฐํ•ฉ๋„๋ผ๋Š” ๋‘ ๊ฐ€์ง€ ์›์น™์„ ํšจ๊ณผ์ ์œผ๋กœ ์‹คํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ• ์ค‘ ํ•˜๋‚˜๊ฐ€ ๋ฐ”๋กœ Headless Pattern์ž…๋‹ˆ๋‹ค.

Headless Pattern(Component)

image

Headless Component๋ž€?

Headless Component๋Š” โ€œ๋จธ๋ฆฌ ์—†๋Š”โ€ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์„œ โ€œ๋จธ๋ฆฌ(head)โ€๋Š” ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ณด์—ฌ์ง€๋Š” UI๋ฅผ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค. ์ฆ‰, Headless Component๋Š” UI ๋ Œ๋”๋ง ์ฑ…์ž„์„ ๋ถ„๋ฆฌํ•˜๊ณ , ์ˆœ์ˆ˜ํ•˜๊ฒŒ ๋กœ์ง๊ณผ ์ƒํƒœ ๊ด€๋ฆฌ์—๋งŒ ์ง‘์ค‘ํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ์ž…๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ Headless Component์—์„œ๋Š” UI๋ฅผ ์™ธ๋ถ€์— ์œ„์ž„ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

UI์™€ ๋กœ์ง์„ ๋ถ„๋ฆฌํ•จ์œผ๋กœ์จ UI๋Š” ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•ด์ง€๊ณ , ๋กœ์ง์€ ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ์™€ ์‰ฝ๊ฒŒ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.โ€

// ๋กœ์ง๊ณผ ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ๋‹ด๋‹นํ•˜๋Š” hook
const useDropdown = () => {
  const [isOpen, setIsOpen] = useState(false);
  const toggle = useCallback(() => setIsOpen(prev => !prev), []);
  return {
    isOpen,
    toggle
  };
};

// Headless Component ํŒจํ„ด์œผ๋กœ ์„ค๊ณ„๋œ Dropdown ์ปดํฌ๋„ŒํŠธ
const Dropdown: React.FC<DropdownProps> = ({ children }) => {
  // useDropdown ํ›…์—์„œ ๋“œ๋กญ๋‹ค์šด ์ƒํƒœ์™€ ํ† ๊ธ€ ๊ธฐ๋Šฅ์„ ๊ฐ€์ ธ์˜ด
  const dropdownProps = useDropdown();
  // children ํ•จ์ˆ˜์— dropdownProps๋ฅผ ์ „๋‹ฌํ•˜์—ฌ ์ž์‹ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ƒํƒœ๋ฅผ ์ œ์–ดํ•˜๋„๋ก ํ•จ
  return <>{children(dropdownProps)}</>;
};

// ์‹ค์ œ ์‚ฌ์šฉ ์˜ˆ์‹œ
const MyDropdown = () => {
  return (
    <Dropdown>
      {({ isOpen, toggle }) => (
        <div>
          <button onClick={toggle}>๋ฉ”๋‰ด</button>
          {isOpen && (
            <div>
              <div>์˜ต์…˜ 1</div>
              <div>์˜ต์…˜ 2</div>
            </div>
          )}
        </div>
      )}
    </Dropdown>
  );
};

์ด๋Ÿฌํ•œ ํŒจํ„ด์€ Input, Button ๋“ฑ ๋‹ค์–‘ํ•œ ์ปดํฌ๋„ŒํŠธ์—๋„ ์ ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์žฅ์ 

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

๋‹จ์ ๊ณผ ๊ณ ๋ ค์‚ฌํ•ญ

  • ๋ณต์žก์„ฑ ์ฆ๊ฐ€: UI์™€ ๋กœ์ง์„ ๋ถ„๋ฆฌํ•˜๋‹ค ๋ณด๋ฉด ์ฒ˜์Œ์—๋Š” ๊ตฌ์กฐ๊ฐ€ ๋ณต์žกํ•ด์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์˜ค๋ฒ„์—”์ง€๋‹ˆ์–ด๋ง ์œ„ํ—˜: ๊ฐ„๋‹จํ•œ ๊ธฐ๋Šฅ์— ๋Œ€ํ•ด ๊ณผ๋„ํ•œ ์ถ”์ƒํ™”๊ฐ€ ์ด๋ฃจ์–ด์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ํ•™์Šต ๊ณก์„ : ์ƒˆ๋กœ์šด ๊ฐœ๋ฐœ์ž๋Š” ์ด ํŒจํ„ด์„ ์ดํ•ดํ•˜๋Š” ๋ฐ ์‹œ๊ฐ„์ด ๊ฑธ๋ฆด ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ, Headless Pattern์€ ๋ณต์žกํ•œ UI์™€ ์ž์ฃผ ๋ณ€๊ฒฝ๋˜๋Š” ๋กœ์ง์ด ์žˆ๋Š” ์ƒํ™ฉ์—์„œ ํŠนํžˆ ์œ ์šฉํ•˜๋ฉฐ, ๊ฐ„๋‹จํ•œ ์ปดํฌ๋„ŒํŠธ๋‚˜ ์ƒํƒœ ๊ด€๋ฆฌ์—๋Š” ์˜คํžˆ๋ ค ๋ณต์žก์„ฑ์„ ์ฆ๊ฐ€์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ฒฐ๋ก 

Headless Pattern์€ ์†Œํ”„ํŠธ์›จ์–ด ๋ณ€๊ฒฝ์— ์œ ์—ฐํ•˜๊ฒŒ ๋Œ€์‘ํ•  ์ˆ˜ ์žˆ๋Š” ๋งค์šฐ ํšจ๊ณผ์ ์ธ ํŒจํ„ด์ž…๋‹ˆ๋‹ค. ์ด ํŒจํ„ด์€ ์†Œํ”„ํŠธ์›จ์–ด ์—”์ง€๋‹ˆ์–ด๋ง์˜ ๊ธฐ๋ณธ ์›์น™์ธ ๋†’์€ ์‘์ง‘๋„์™€ ๋‚ฎ์€ ๊ฒฐํ•ฉ๋„๋ฅผ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋‹ฌ์„ฑํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋„์™€์ค๋‹ˆ๋‹ค. ํŠนํžˆ UI/UX๊ฐ€ ์ž์ฃผ ๋ณ€๊ฒฝ๋˜๋Š” ํ˜„๋Œ€ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ฐœ๋ฐœ์— ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.

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

๋”ฐ๋ผ์„œ, ์ด ํŒจํ„ด์€ ๊ทธ ์œ ์šฉ์„ฑ์„ ์ž˜ ์ดํ•ดํ•˜๊ณ , ์ ์ ˆํ•œ ์ƒํ™ฉ์—์„œ ์‚ฌ์šฉํ•˜๋ฉด ๋งค์šฐ ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ๊ฐ€ ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ฐธ๊ณ  ์ž๋ฃŒ

(๋ฒˆ์—ญ) ํ—ค๋“œ๋ฆฌ์Šค ์ปดํฌ๋„ŒํŠธ: ๋ฆฌ์•กํŠธ UI๋ฅผ ํ•ฉ์„ฑํ•˜๊ธฐ ์œ„ํ•œ ํŒจํ„ด

๐Ÿ˜Ž ์›จ๋ฒ ๋ฒ ๋ฒ ๋ฒฑ

๐Ÿ‘ฎ๐Ÿป ํŒ€ ๊ทœ์น™

๐Ÿ’ป ํ”„๋กœ์ ํŠธ

๐Ÿชต ์›จ๋ฒ ๋ฒฑ ๊ธฐ์ˆ ๋กœ๊ทธ

๐Ÿช„ ๋ฐ๋ชจ ๊ณต์œ 

๐Ÿ”„ ์Šคํ”„๋ฆฐํŠธ ๊ธฐ๋ก

๐Ÿ“— ํšŒ์˜๋ก

Clone this wiki locally