Front-end

잘못된 추상화 바로잡기 - DRY(Don't Repeat Yourself)

ghDev 2024. 8. 7. 19:29

출처: https://swizec.com/blog/dry-the-common-source-of-bad-abstractions/

 

이번 글에선 외부 자료를 참고하여 리팩토링의 첫번째인 반복되는 코드줄이기 (DRY: Don't Repeat Yourself)를 해본다.

 

왜 DRY를 사용할까?

console.log(1)
console.log(2)
console.log(3)
console.log(4)
// ...

 

우리는 항상 이런 코드는 반복문을 사용해서 DRY 코드로 바꿔야 한다.

 

for (let i = 1; i < 5; i++) {
  console.log(i);
}

 

아주 기본적인 예시로 실무에서 이런 코드는 쓸 일이 없을거다. 하지만 여기에 몇가지 예시를 더 해본다면 비슷하게 적용할 수 있다.

 

예를 들어

const NavigationMenu = () => {
  return (
    <ul>
      <li>
        <a href="/about">
          <img src="question-icon.png" />
          About
        </a>
      </li>
      <li>
        <a href="/contact">
          <img src="person-icon.png" />
          Contact
        </a>
      </li>
      <li>
        <a href="/buy">
          <img src="cash-icon.png" />
          Buy
        </a>
      </li>
      // ...
    </ul>
  );
};

 

처음 코딩을 시작했을때 했을만한 익숙한 코드이다. 이 반복적인 코드의 단점은?

  • 유지관리하기 어렵다.
  • 읽기도 어렵다.
  • 수정을 하거나 할때 실수하기 좋은 코드이다.

이 코드를 DRY하게 만든다면?

내가 흔히 작성했던 잘못된 예시이다.

const NavigationMenu = () => {
  const items = [
    {
      url: "/about",
      icon: "question-icon.png",
      label: "About",
    },
    {
      url: "/contact",
      icon: "person-icon.png",
      label: "Contact",
    },
    {
      url: "/buy",
      icon: "cash-icon.png",
      label: "Buy",
    },
    // ...
  ];

  return (
    <ul>
      {items.map((item) => (
        <li>
          <a href={item.url}>
            <img src={item.icon} />
            {label}
          </a>
        </li>
      ))}
    </ul>
  );
};

 

물론 이 코드는 반복작업이 줄고 오류가 덜 발생할 것이다. 또한 한 줄의 코드로 모든 요소의 마크업을 정의하고 변경할 수 있다.

 

팩토리 패턴을 통한 더 많은 DRY

하지만 위의 코드는 NavItems라는 객체가 거슬렸고 링크를 추가하게 된다면 객체를 복사해서 문자열 값을 변경해야 할 것이다.
이걸 팩토리 패턴으로 리팩토링 해본다면?

function makeNavItem(url, icon, label) {
  return { url, icon, label };
}

const NavigationMenu = () => {
  const items = [
    makeNavItem("/about", "question-icon.png", "About"),
    makeNavItem("/contact", "person-icon.png", "Contact"),
    makeNavItem("/buy", "cash-icon.png", "Buy"),
    // ...
  ];

  return (
    <ul>
      {items.map((item) => (
        <li>
          <a href={item.url}>
            <img src={item.icon} />
            {label}
          </a>
        </li>
      ))}
    </ul>
  );
};


자바스크립트의 객체 생성 구문 덕분에 코드량이 짧아지고 DRY를 지키게 되었다.

  • 팩토리는 각 구성 객체를 반환하고
  • 반환된 객체들을 리스트로 만든다.
  • 데이터를 순회하며 항목을 렌더링한다.

이 코드는 항목을 쉽게 추가하고 제거할 수 있게 되었지만 모든 곳에서 이 패턴을 사용하지 않는 한 코드를 읽기는 더 어려워졌다.
코드가 어떻게 작동하는지 이해하려면 위에서 아래로 코드를 읽는게 아닌 하단 코드를 확인 한 뒤 상단 코드를 다시 확인해야한다.

 

이것이 나쁜 추상화인 이유

만약 Buy 버튼에만 다른 css를 넣고 싶다면?

이 추상화는 모든 버튼을 동일하게 유지하는 데에 최적화되어 있어 각 버튼이 다른 방향으로 발전할 여지가 없다.

이는 팩토리 패턴에서 흔히 일어나는 일로 기본 코드를 직접 작성하는 것이 나을 정로도 복잡해진다.

 

저 코드의 작성자는 저 코드가 어떻게 진화하는지 충분히 오래 관찰하지 않았다. 개발 당시에는 버튼이 각각의 방향을 가질거란 생각을 못했기 때문이다.

 

더 나은 추상화 만들기 - 관심사의 분리

이번에는 관심사를 분리하는 방법으로 DRY 코드를 작성해본다.

고려해야할 두가지는

  1. 메뉴
  2. 버튼
const NavMenu = ({ title, href, prefetch = true, icon }: NavMenuProps) => {
  const pathname = usePathname()
  const isActive = href === "/" ? pathname === href : pathname.startsWith(href)

  return (
    <Link
      href={href}
      prefetch={prefetch}
      className={`flex h-full flex-col items-center text-gray-500 focus:outline-none ${
        isActive ? "text-main" : "text-gray-500"
      }`}
    >
      <div className="mt-4 flex flex-col items-center gap-[2px]">
        {icon({
          className: "w-5 h-5",
        })}
        <span className="mt-[2px] text-xs font-medium">{title}</span>
      </div>
    </Link>
  )
}

const NavigationMenu = () => {
	return (
        <NavMenu title="홈" href="/" icon={HomeIcon} />
        <NavMenu title="채용" href="/jobs" icon={BriefcaseIcon} />
        <NavMenu title="레슨" href="/lessons" icon={BookIcon} />
        <NavMenu title="뉴스" href="/news" icon={NewsPaperIcon} />
	)
}

 

이러한 추상화를 통해 예외를 쉽게 만들수 있다. 반복 중 하나에서 다르게 동작하도록 반복문을 조작할 필요가 없고

 

합성 패턴(children)을 추가하여 풍부한 레이블을 쉽게 렌더링 할 수도 있다.

 

관심사는 다음과 같이 분리된다.

  • 메뉴의 구조를 위한 NavigationMenu
  • 각 항목의 구조를 위한 MenuItem
  • 항목의 값에 대해 렌더링된 각 항목