잘못된 추상화 바로잡기 - DRY(Don't Repeat Yourself)
출처: 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 코드를 작성해본다.
고려해야할 두가지는
- 메뉴
- 버튼
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
- 항목의 값에 대해 렌더링된 각 항목