[번역] DRY - 잘못된 추상화의 일반적인 원인

eunbinn·2024년 8월 5일
80

FrontEnd 번역

목록 보기
35/38
post-thumbnail

원문: https://swizec.com/blog/dry-the-common-source-of-bad-abstractions/

제가 보거나 작성한 코드 중 가장 최악이고 유지 관리하기 어려운 코드는 DRY(Don't Repeat Yourself, 반복하지 않기)의 원칙을 추구한 코드였습니다. DRY는 엔지니어가 가장 먼저 배우는 설계 원칙 중 하나이며, 우리는 이 원칙을 너무 좋아하고 있습니다.

DRY 코드 읽기 vs 단순한 코드 읽기
DRY 코드 읽기 vs 단순한 코드 읽기

왜 DRY를 사용하나요?

DRY는 기본을 배울 때 키우기 좋은 근육입니다.

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

항상 이런 코드는 아래와 같이 루프를 사용하는 코드로 DRY해서 바꿔야 합니다.

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

네, 초급 프로그래밍 수업에서나 볼 수 있는 바보 같은 예시이고 실무에서 이런 코드를 작성하지는 않을 것입니다. 하지만 여기에 몇 가지 레이어를 더해본다면 실무에서 사용하는 것과 비슷한 코드가 될 수 있습니다.

Joe라는 동료가 내비게이션 메뉴를 최대한 대충 구현한다고 가정해 보겠습니다.

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>
  );
};

모든 새 항목은 라벨, URL 및 아이콘이 약간 변경된 이전 항목의 복사본입니다. 매우 반복적이죠.

Jane은 PR을 보고 다음과 같은 리뷰를 남깁니다.

  • 이 코드는 유지 관리하기 어렵습니다.
  • 코드를 읽기도 어렵습니다.
  • 업데이트를 서두른다면 실수하기 쉬운 코드입니다.

이 메뉴의 업데이트는 자다가도 할 수 있는 무의미한 작업처럼 느껴지기 때문에 서두르게 된다는 것을 우리 모두 알고 있습니다.

바로 그럴 때 실수가 발생하죠. 😉

DRY가 잘못되는 과정

Jane은 이 코드를 반복문을 사용하여 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>
  );
};

코드가 10줄에서 28줄로 늘었습니다. 하지만 반복적인 작업이 줄었고 오류도 덜 발생합니다! 한 줄의 코드로 모든 요소의 마크업을 정의할 수 있으므로 한 번만 변경하면 됩니다.

Jane은 배열을 구성하는 객체가 썩 마음에 들진 않지만 팀원들은 이 정도는 괜찮다고 생각합니다. PR이 승인됩니다. ✅

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

병합 버튼을 누르기 전에 Alice는 그 객체가 정말 거슬린다는 생각을 하게 됩니다. Jane과 Joe는 반복적인 부분을 제거하지 않고 그냥 이리저리 흩뿌렸습니다.

다음에 링크를 추가할 사람은 객체를 복사한 다음 문자열 값을 변경합니다. 별로 좋진 않죠.

그녀는 오래된 팩토리 패턴을 꺼내서 각 구성 요소를 반환하는 함수를 작성하기로 결정합니다. 나중에 더 많은 로직이 포함된 더 스마트한 팩토리로 확장할 수 있습니다.

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>
  );
};

자바스크립트의 편리한 객체 생성 구문 덕분에 다시 20줄의 코드로 돌아갔습니다. 이는 기존 10줄의 두 배이지만 DRY를 지킵니다.

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

이제 항목을 쉽게 추가하고 제거할 수 있습니다. 동적으로 만들고 콘텐츠 관리 시스템에서 목록을 가져올 수도 있습니다.

하지만 Jane, Joe, Alice가 모든 곳에서 이 패턴을 사용하지 않는 한 코드를 읽기는 더 어려워졌습니다. 코드가 어떻게 작동하는지 이해하려면 이리저리 뛰어다니며 정신을 집중해야 합니다. 위에서 아래로 순차적으로 읽는 것과는 대조적입니다.

DRY 코드 읽기 vs 단순한 코드 읽기
DRY 코드 읽기 vs 단순한 코드 읽기

하지만 코드 읽기가 혼란스러운 것보다 더 큰 문제가 있습니다. 이것은 잘못된 추상화입니다.

이것이 나쁜 추상화인 이유 - 관심사의 분리

몇 달이 지나고 마케팅 팀에서 다음과 같은 실험을 진행하려고 합니다. Buy 버튼에 빨간 테두리를 넣으면 클릭이 더 많이 발생할까요?

Joe는 코드를 보고 가슴이 철렁 내려앉습니다. 이 추상화는 모든 버튼을 동일하게 유지하는 데 최적화되어 있습니다. 버튼이 모두 결합되어 있어서 각 버튼이 다른 방향으로 발전할 여지가 없습니다.

이제 Joe에게 선택권이 생겼습니다.

  • DRY를 버리고 간단한 코드로 다시 작성합니다.
  • 버튼을 구성하기 위해 더 많은 매개변수를 추가합니다.
  • 추상화를 다시 작성합니다.

이 중 두 개는 "잘못된 추상화는 고칠 수 없다"는 생각을 따르고 있습니다. 하나는 상황을 더 악화시켜 사용할 때마다 동작을 신중하게 조정하기 위해 수많은 부울 인수를 사용하는 팩토리의 길을 가고 있습니다.

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

이 팀의 실수는 이 코드가 어떻게 진화하는지 충분히 오래 관찰하지 않았다는 것입니다. 당시에는 모든 내비게이션 버튼이 똑같이 보여야 할 것 같았습니다. 하지만 한 버튼은 다른 버튼과 의미가 달랐습니다.

AboutContact는 내비게이션 항목입니다. Buy는 행동으로 사용자 흐름을 만듭니다. 이는 미묘하지만 중요한 차이로, Buy 버튼의 동작이 다른 버튼과 서로 다를 수 있음을 나타냅니다.

이러한 미묘한 차이는 미리 알아차리기가 거의 불가능합니다. 하지만 돌이켜보면 항상 분명합니다.

더 나은 추상화 만들기

Joe는 이 기회에 코드베이스 정리를 해봅니다. 이전의 추상화는 미숙했지만 마케팅에서 요청한 스타일 변경을 통해 그는 관심사를 분리하는 방법에 대한 통찰력을 얻었습니다.

두 가지 고려해야 할 사항이 있습니다.

  1. 메뉴
  2. 버튼

이것들은 리액트 컴포넌트가 될 수 있습니다.

const MenuItem = ({ href, style, icon, children }) => (
	<li style={style}><a href={href}><img src={icon} />{children}</a>
)

const NavigationMenu = () => {
	return (
		<ul>
			<MenuItem href="/about" icon="question-icon.png">About</MenuItem>
			<MenuItem href="/contact" icon="person-icon.png">Contact</MenuItem>
			<MenuItem href="/buy" icon="cash-icon.png" style={{ border: '1px solid red' }}>Buy</MenuItem>
		</ul>
	)
}

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

또한 (children을 사용하는) 합성 패턴을 사용하면 풍부한 레이블을 쉽게 렌더링할 수 있습니다. 원하는 경우 Buy 버튼에 마크업을 추가할 수 있습니다.

인기 있는 디자인 라이브러리에서는 icon 프로퍼티에 리액트 컴포넌트를 사용하여 이를 더욱 활용합니다. 이렇게 하면 아이콘 렌더링을 더 잘 제어할 수 있습니다.

관심사가 다음과 같이 분리됩니다.

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

이것이 바로 수년간의 개발 과정에서 수천 개의 코드베이스가 어떻게 진화했는지를 관찰한 끝에 디자인 라이브러리에서 정착한 패턴입니다. 사용자의 욕구(desire path)를 관찰하는 것은 효과가 있습니다. :)

6개의 댓글

comment-user-thumbnail
2024년 8월 6일

좋은 정보 감사합니다

1개의 답글
comment-user-thumbnail
2024년 8월 6일

항상 고민하던 것 중에 하나였는데 좋은 지적(정보) 감사합니다. ^^

답글 달기
comment-user-thumbnail
2024년 8월 6일

우연히 같은 건지 (달라질 가능성이 높은지) 진짜 같은 로직인건지 구별하는 능력이 필요하겠군요

답글 달기
comment-user-thumbnail
2024년 8월 14일

이제 누군가 menuItem에 내려줄 정보를 navigationMenu를 통해 드랍하겠죠.

세상은 늘 돌고 돌거라는... ㅎㅎㅎ

답글 달기
comment-user-thumbnail
2024년 8월 15일

thanks for sharing

답글 달기