[TECH-QA] ๊ฐœ๋ฐฉ-ํ์‡„ ์›์น™(Open-Closed Principle, OCP)

OCP๋ฅผ ์œ„๋ฐ˜ํ•˜๋Š” Button ์ปดํฌ๋„ŒํŠธ(์•ˆํ‹ฐํŒจํ„ด)

๊ด€๋ จ ํ‚ค์›Œ๋“œ

OCP ์œ„๋ฐ˜

Button ์ปดํฌ๋„ŒํŠธ

๋จผ์ €, ์ƒˆ๋กœ์šด ๋ฒ„ํŠผ variant๋ฅผ ์ถ”๊ฐ€ํ•  ๋•Œ๋งˆ๋‹ค ๊ธฐ์กด ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•ด์•ผ ํ•˜๋Š” ์•ˆํ‹ฐํŒจํ„ด์„ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ์ด ๋ฐฉ์‹์€ OCP๋ฅผ ์œ„๋ฐ˜ํ•˜๋ฉฐ ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ๋–จ์–ด๋œจ๋ฆฝ๋‹ˆ๋‹ค.
// ์•ˆํ‹ฐํŒจํ„ด: OCP๋ฅผ ์œ„๋ฐ˜ํ•˜๋Š” Button ์ปดํฌ๋„ŒํŠธ
function Button({ variant, children, ...props }) {
  let className = 'button';

  if (variant === 'primary') {
    className += ' button--primary';
  } else if (variant === 'secondary') {
    className += ' button--secondary';
  } else if (variant === 'danger') {
    className += ' button--danger';
  }

  return (
    <button className={className} {...props}>
      {children}
    </button>
  );
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
<Button variant="primary">Primary Button</Button>
<Button variant="secondary">Secondary Button</Button>
<Button variant="danger">Danger Button</Button>

๐Ÿ“ ๋ฌธ์ œ์ 

  • ์ƒˆ๋กœ์šด variant (์˜ˆ: success)๋ฅผ ์ถ”๊ฐ€ํ•˜๋ ค๋ฉด Button ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์˜ if-else ๋กœ์ง์„ ์ˆ˜์ •ํ•ด์•ผ ํ•จ.
  • ์ด๋Š” OCP์˜ "๋ณ€๊ฒฝ์— ๋‹ซํ˜€ ์žˆ์–ด์•ผ ํ•œ๋‹ค"๋ฅผ ์œ„๋ฐ˜.
  • ์ฝ”๋“œ๊ฐ€ ๋ณต์žกํ•ด์ง€๊ณ , ์ƒˆ๋กœ์šด ์š”๊ตฌ์‚ฌํ•ญ์ด ์ƒ๊ธธ ๋•Œ๋งˆ๋‹ค ๊ธฐ์กด ์ฝ”๋“œ ์ˆ˜์ •์ด ๋ถˆ๊ฐ€ํ”ผํ•ด์ง.

OCP๋ฅผ ์ค€์ˆ˜ํ•˜๋Š” ๋ฆฌํŒฉํ„ฐ๋ง ButtonBase ์ปดํฌ๋„ŒํŠธ

๊ด€๋ จ ํ‚ค์›Œ๋“œ

OCP ์ค€์ˆ˜

ButtonBase ์ปดํฌ๋„ŒํŠธ

OCP๋ฅผ ์ค€์ˆ˜ํ•˜๊ธฐ ์œ„ํ•ด ButtonBase๋ผ๋Š” ๊ธฐ๋ณธ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค๊ณ , ์ด๋ฅผ ํ™•์žฅํ•˜์—ฌ ๋‹ค์–‘ํ•œ ๋ฒ„ํŠผ ๋ณ€ํ˜•์„ ์ƒ์„ฑํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๋ฆฌํŒฉํ„ฐ๋งํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ์ƒˆ๋กœ์šด variant๋ฅผ ์ถ”๊ฐ€ํ•  ๋•Œ ๊ธฐ์กด ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•˜์ง€ ์•Š๊ณ ๋„ ํ™•์žฅ์ด ๊ฐ€๋Šฅํ•ด์ง‘๋‹ˆ๋‹ค.
// ๊ธฐ๋ณธ ์ปดํฌ๋„ŒํŠธ ButtonBase
function ButtonBase({ className, children, ...props }) {
  return (
    <button className={`button ${className}`} {...props}>
      {children}
    </button>
  );
}

// ํ™•์žฅ๋œ ๋ฒ„ํŠผ ์ปดํฌ๋„ŒํŠธ๋“ค
const PrimaryButton = ({ children, ...props }) => (
  <ButtonBase className="button--primary" {...props}>
    {children}
  </ButtonBase>
);

const SecondaryButton = ({ children, ...props }) => (
  <ButtonBase className="button--secondary" {...props}>
    {children}
  </ButtonBase>
);

const DangerButton = ({ children, ...props }) => (
  <ButtonBase className="button--danger" {...props}>
    {children}
  </ButtonBase>
);

// ์‚ฌ์šฉ ์˜ˆ์‹œ
<PrimaryButton>Primary Button</PrimaryButton>
<SecondaryButton>Secondary Button</SecondaryButton>
<DangerButton>Danger Button</DangerButton>

// ์ƒˆ๋กœ์šด SuccessButton ์ถ”๊ฐ€ (๊ธฐ์กด ์ฝ”๋“œ ์ˆ˜์ • ์—†์ด ๊ฐ€๋Šฅ)
const SuccessButton = ({ children, ...props }) => (
  <ButtonBase className="button--success" {...props}>
    {children}
  </ButtonBase>
);

๐Ÿ“ ์žฅ์ 

  • ํ™•์žฅ์„ฑ: ์ƒˆ๋กœ์šด ๋ฒ„ํŠผ ๋ณ€ํ˜•(์˜ˆ: SuccessButton)์„ ์ถ”๊ฐ€ํ•  ๋•Œ ButtonBase๋‚˜ ๊ธฐ์กด ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ˆ˜์ •ํ•˜์ง€ ์•Š์•„๋„ ๋จ.
  • ์œ ์ง€๋ณด์ˆ˜์„ฑ: ๊ฐ ๋ฒ„ํŠผ ๋ณ€ํ˜•์ด ๋…๋ฆฝ์ ์ด์–ด์„œ ์ฝ”๋“œ๊ฐ€ ๊ฐ„๊ฒฐํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๊ธฐ ์‰ฌ์›€.
  • OCP ์ค€์ˆ˜: ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•˜๊ธฐ ์œ„ํ•ด ๊ธฐ์กด ์ฝ”๋“œ๋ฅผ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š๊ณ  ํ™•์žฅ ๊ฐ€๋Šฅ.

์ปดํฌ๋„ŒํŠธ ํ•ฉ์„ฑ ํŒจํ„ด Card ์ปดํฌ๋„ŒํŠธ

๊ด€๋ จ ํ‚ค์›Œ๋“œ

์ปดํฌ๋„ŒํŠธ ํ•ฉ์„ฑ ํŒจํ„ด

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

๐Ÿ“ ๊ธฐ๋ณธ Card ์ปดํฌ๋„ŒํŠธ

function Card({ header, body, footer, ...props }) {
  return (
    <div className="card" {...props}>
      {header && <div className="card-header">{header}</div>}
      {body && <div className="card-body">{body}</div>}
      {footer && <div className="card-footer">{footer}</div>}
    </div>
  );
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
<Card
  header={<h2>Card Title</h2>}
  body={<p>This is the card content.</p>}
  footer={<PrimaryButton>Action</PrimaryButton>}
/>

๐Ÿ“ ์ƒˆ๋กœ์šด ํ™•์žฅ: ์ด๋ฏธ์ง€ ํฌํ•จ Card

const ImageCard = ({ image, ...props }) => (
  <Card
    header={image && <img src={image} alt="Card Image" className="card-image" />}
    {...props}
  />
);

// ์‚ฌ์šฉ ์˜ˆ์‹œ
<ImageCard
  image="https://example.com/image.jpg"
  body={<p>This is an image card.</p>}
  footer={<SecondaryButton>Learn More</SecondaryButton>}
/>

๐Ÿ“ ์žฅ์ 

  • ์œ ์—ฐ์„ฑ: Card ์ปดํฌ๋„ŒํŠธ๋Š” ๋‹ค์–‘ํ•œ ์ฝ˜ํ…์ธ (ํ—ค๋”, ๋ฐ”๋””, ํ‘ธํ„ฐ)๋ฅผ ์กฐํ•ฉํ•˜์—ฌ ํ™•์žฅ ๊ฐ€๋Šฅ.
  • ์žฌ์‚ฌ์šฉ์„ฑ: ImageCard์™€ ๊ฐ™์€ ์ƒˆ๋กœ์šด ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ธฐ์กด Card๋ฅผ ํ™œ์šฉํ•ด ์‰ฝ๊ฒŒ ์ƒ์„ฑ.
  • OCP ์ค€์ˆ˜: ์ƒˆ๋กœ์šด Card ๋ณ€ํ˜•์„ ์ถ”๊ฐ€ํ•  ๋•Œ ๊ธฐ์กด Card ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ˆ˜์ •ํ•  ํ•„์š” ์—†์Œ.

๊ณ ์ฐจ ์ปดํฌ๋„ŒํŠธ(HOC)๋ฅผ ํ†ตํ•œ ํ™•์žฅ

๊ณ ์ฐจ ์ปดํฌ๋„ŒํŠธ(HOC)๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๊ธฐ์กด ์ปดํฌ๋„ŒํŠธ์— ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•˜์—ฌ OCP๋ฅผ ์ค€์ˆ˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ๋ฒ„ํŠผ์— ๋กœ๋”ฉ ์ƒํƒœ๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” HOC๋ฅผ ๋งŒ๋“ค์–ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.
// HOC: ๋กœ๋”ฉ ์ƒํƒœ๋ฅผ ์ถ”๊ฐ€
function withLoading(Component) {
  return function LoadingComponent({ isLoading, children, ...props }) {
    return (
      <Component {...props} disabled={isLoading}>
        {isLoading ? 'Loading...' : children}
      </Component>
    );
  };
}

// ๋กœ๋”ฉ ๊ฐ€๋Šฅํ•œ ๋ฒ„ํŠผ ์ƒ์„ฑ
const LoadingPrimaryButton = withLoading(PrimaryButton);
const LoadingSecondaryButton = withLoading(SecondaryButton);

// ์‚ฌ์šฉ ์˜ˆ์‹œ
<LoadingPrimaryButton isLoading={true}>Click Me</LoadingPrimaryButton>
<LoadingSecondaryButton isLoading={false}>Click Me</LoadingSecondaryButton>

๐Ÿ“ ์žฅ์ 

  • ๊ธฐ๋Šฅ ํ™•์žฅ: withLoading HOC๋ฅผ ํ†ตํ•ด ๊ธฐ์กด ๋ฒ„ํŠผ ์ปดํฌ๋„ŒํŠธ์— ๋กœ๋”ฉ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€.
  • OCP ์ค€์ˆ˜: ๊ธฐ์กด PrimaryButton์ด๋‚˜ SecondaryButton ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•˜์ง€ ์•Š๊ณ  ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ ์ถ”๊ฐ€ ๊ฐ€๋Šฅ.
  • ์žฌ์‚ฌ์šฉ์„ฑ: HOC๋Š” ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ์—๋„ ์ ์šฉ ๊ฐ€๋Šฅ.