[Next.js] 포트폴리오 웹 페이지 제작기 - 11. 리팩토링

olwooz·2023년 2월 26일
1

반응형 웹 구현을 시작하고 보니 버튼 관련 코드 리팩토링이 시급해서 다른 섹션의 반응형 레이아웃을 구현하기 전에 리팩토링부터 바로 착수하기로 했다.

버튼 관련 코드 리팩토링

반응형 화면을 구성하는 과정에서 비슷하지만 다른 버튼들을 여러 곳에서 사용하게 되다 보니 버튼 관련 코드를 리팩토링해야 할 때가 왔다.
그리고 현재 아이콘들을 버튼으로 사용하는 경우도 많은데, 아이콘 관련 코드도 같이 손을 봐야겠다.

섹션 버튼

3가지 문제점이 있다.

  1. 섹션 버튼 정의가 Header/HeaderButtonSlideMenu/MenuButton에 흩어져 있다.
  2. 각 섹션당 하나의 버튼을 일일이 정의하고 있다.
  3. ButtonGroupHeader/HeaderButton 4개를 나열해놨다.

어차피 섹션 버튼들은 함께 사용되기 때문에, 섹션 버튼을 일반화하고 섹션의 string 배열에 map을 사용해 SectionButtons이라는 배열로 만들었다.

// components/Buttons/SectionButtons.tsx

import Link from 'next/link';

interface Props {
  wrapperStyle?: string;
  buttonStyle?: string;
  onClick?: (...args: any) => void;
}

const sections = ['main', 'about', 'projects', 'contact'];

const SectionButtons = ({ wrapperStyle, buttonStyle, onClick }: Props) => {
  return (
    <>
      {sections.map((section) => (
        <div key={section} className={wrapperStyle}>
          <Link href={`#${section}`} scroll={false} onClick={onClick}>
            <button className={'capitalize ' + buttonStyle}>{section}</button>
          </Link>
        </div>
      ))}
    </>
  );
};

export default SectionButtons;

이제 기존 코드를 변경된 SectionButtons로 대체한다.

// components/Header/Header.tsx

/* BEFORE */
<HeaderButton name="main" />
<HeaderButton name="about" />
<HeaderButton name="projects" />
<HeaderButton name="contact" />

/* AFTER */
<SectionButtons wrapperStyle="inline-block" buttonStyle="px-4" />
// components/SlideMenu/SlideMenu.tsx

/* BEFORE */
<MenuButton name="main" />
<MenuButton name="about" />
<MenuButton name="projects" />
<MenuButton name="contact" />

/* AFTER */
<SectionButtons wrapperStyle="block" buttonStyle="py-16 text-2xl" onClick={toggleOpen} />

아이콘 버튼

2가지 문제점이 있다.

  1. 크기와 스타일을 아이콘 컴포넌트의 내부에 정의하고 있어서 커스텀이 안 되고 있다.
  2. 아이콘 svg 코드와 기능 코드가 공존하는 부분이 있다.

아이콘 또한 크기와 스타일을 props로 받게끔 변경하고, 기능을 분리해 버튼으로 옮겨주는 작업을 할 것이다. 아이콘 버튼 또한 아이콘 이름의 배열을 순회하며 렌더링하기 때문에, 코드 중복을 줄이기 위해 각 아이콘에 맞는 함수를 짝지어주는 custom hook을 만들 것이다.

아이콘 버튼은 섹션 버튼과 다르게 버튼 리스트가 일관되지 않고, 개별적으로 존재하는 버튼들도 있기 때문에 한 데 묶어서 IconButtons로 만들지는 않을 것이다.

// hooks/useButtonActions.ts

import { IconNames } from '@/components/Icons/types';
import useLanguage from './useLanguage';
import { useStoreDarkMode, useStoreSlideMenu } from './useStore';

type ButtonActions = {
  [key in IconNames]?: (...args: any) => any;
};

const useButtonActions = (): ButtonActions => {
  const { toggleDarkMode } = useStoreDarkMode();
  const { toggleOpen } = useStoreSlideMenu();
  const { currentLanguage, changeLocale, languageList } = useLanguage();

  function openLink(url: string) {
    window.open(url);
  }

  function changeToNextLanguage() {
    const nextIndex = languageList.indexOf(currentLanguage) + 1;
    changeLocale(languageList[nextIndex % languageList.length]);
  }

  const buttonActions: ButtonActions = {
    GitHubIcon: () => openLink('https://github.com'),
    HamburgerIcon: toggleOpen,
    LanguageIcon: changeToNextLanguage,
    LightDarkIcon: toggleDarkMode,
    RightArrowIcon: toggleOpen,
    VelogIcon: () => openLink('https://velog.io'),
  };

  return buttonActions;
};

export default useButtonActions;

이에 맞춰 IconButton 컴포넌트를 수정해준다.

// component/Buttons/IconButton.tsx

import { Icons } from '@/components/Icons';
import { IconNames, IconProps } from '@/components/Icons/types';
import useButtonActions from '@/hooks/useButtonActions';
import { ComponentType } from 'react';

interface Props extends IconProps {
  name: IconNames;
  buttonStyle?: string;
}

const IconButton = ({ name, buttonStyle, size, style }: Props) => {
  const buttonActions = useButtonActions();
  const Icon: ComponentType<IconProps> = Icons[name];

  return (
    <button className={buttonStyle} onClick={buttonActions[name]}>
      <Icon size={size} style={style} />
    </button>
  );
};

export default IconButton;

아이콘은 여러 곳에서 같은 스타일을 사용하고 있으므로 스타일을 별도의 파일로 빼준다.

// constants/styles.ts

export const iconStyle = 'transition hover:scale-125 dark:fill-slate-300 dark:hover:fill-slate-100';

마지막으로 각 IconButton을 사용하는 코드를 다음과 같이 변경해준다.

// components/Header/Header.tsx

/* ... */

<div className="sm:hidden">
  <IconButton name="HamburgerIcon" size={36} style={iconStyle} />
</div>

/* ... */

결과

이번 리팩토링을 통해 코드의 중복을 줄였고 컴포넌트들의 확장성과 유지보수성을 높였다.
각 아이콘에 자유자재로 함수를 붙였다 뗐다 할 수 있게 되어서 특히 맘에 든다.
최근 디자인 패턴을 계속 공부하고 있었는데, 다른 onClick 함수들을 가진 버튼이 아이콘을 감싼다는 점에서 데코레이터 패턴과 유사한 느낌의 구현인 것 같다는 생각도 든다.

다음으로는 반응형 웹 레이아웃 작업을 계속 이어나갈 것이다.

0개의 댓글