728x90

최근 프로젝트의 기능 구현이 마무리가 되었습니다. 하지만 아직 클린코드의 관점에서 미숙한 부분이 많습니다.

이를 값, 계산 , 액션으로 분리하기와  solid원칙을 준수하기를 바탕으로 리팩토링하려고 합니다.

 

코드 원문과 이미지는 아래와 같습니다.

import { useState } from 'react';
import Tutorial1 from './Tutorial1';
import Tutorial2 from './Tutorial2';
import styles from '@/app/luckyshiba/luckyshiba.module.css';
import { MANUAL_SKIP } from '@/shared/contants';

export default function GuidePopUp() {
  const [step, setStep] = useState<number>(0);
  const stepChanger = (direction: 'before' | 'after') => {
    const lastStep = 1;
    if (direction === 'before') {
      if (step > 0) {
        setStep(step - 1);
      }
    } else {
      if (step < lastStep) {
        setStep(step + 1);
      }
    }
  };

  const handleDontShowAgain = () => {
    localStorage.setItem(MANUAL_SKIP, 'true');

    const escEvent = new KeyboardEvent('keydown', { key: 'Escape' });
    document.dispatchEvent(escEvent);
  };

  return (
    <div className={styles.manualWrapper}>
      <div className={styles.manualTitle}>게임 시작 메뉴얼</div>
      <div className={styles.manualContents}>
        {step === 0 && <Tutorial1 />}
        {step === 1 && <Tutorial2 />}
      </div>
      <button className={styles.hideButton} onClick={handleDontShowAgain}>
        다시 보지 않기
      </button>
      <div className={styles.naviWrapper}>
        <button
          className={styles.naviButton}
          onClick={() => {
            stepChanger('before');
          }}
        >
          이전
        </button>
        {step + 1 + '/' + 2}

        <button
          className={styles.naviButton}
          onClick={() => {
            stepChanger('after');
          }}
        >
          다음
        </button>
      </div>
    </div>
  );
}

이런 지저분한 코드를 관심사를 분리해서

import styles from '@/app/luckyshiba/luckyshiba.module.css';
import { tutorials } from './content';
import { useStep } from './hook/useStep';
import HideButton from './control/HideButton';
import Navigation from './control/Navigation';

export default function GuidePopUp() {
  const lastStep = tutorials.length;
  const [step, stepChanger] = useStep(lastStep);

  return (
    <div className={styles.manualWrapper}>
      <div className={styles.manualTitle}>게임 시작 메뉴얼</div>
      <div className={styles.manualContents}>{tutorials[step]}</div>
      <HideButton />
      <Navigation step={step} stepChanger={stepChanger} totalSteps={lastStep} />
    </div>
  );
}

이렇게 만드는 과정입니다.

 

그러기 위해선 우선 화면을 관심사에 따라서 세분화 합니다.

 공통적으로 존재하는 제목 영역, step에 따라 바뀌어야 하는 내용영역, 그리고 공통적으로 존재하는 버튼과 현재 step 영역이 존재합니다.

 

내용영역 분리하기

이 중에서 우선 가운데의 내용부분을 분리하려고 합니다.

 

내용의 경우에는 줄바꿈이나, 강조하고 싶은 색깔이 다르기 때문에 공통적인 요소로 묶기 힘드니, 개별적인 컴퍼넌트로 작성하였습니다. 이때 내용의 경우에는 html과 css를 활용해서 꾸민 '값'입니다.

따라서 Popup 컴퍼넌트에서 값을 분리하였습니다.

 

import Tutorial1 from './Tutorial1';
import Tutorial2 from './Tutorial2';

export const tutorials = [<Tutorial1 key={1} />, <Tutorial2 key={2} />];

 

import { useState } from 'react';

import styles from '@/app/luckyshiba/luckyshiba.module.css';
import { hideManualByDefault } from './util';
import { tutorials } from './content';

export default function GuidePopUp() {
  const lastStep = tutorials.length;
  const [step, setStep] = useState<number>(0);

  const stepChanger = (direction: 'before' | 'after') => {
    if (direction === 'before') {
      if (step > 0) {
        setStep(step - 1);
      }
    } else {
      if (step < lastStep) {
        setStep(step + 1);
      }
    }
  };

  return (
    <div className={styles.manualWrapper}>
      <div className={styles.manualTitle}>게임 시작 메뉴얼</div>
      <div className={styles.manualContents}>{tutorials[step]}</div>

      <button className={styles.hideButton} onClick={hideManualByDefault}>
        다시 보지 않기
      </button>
      <div className={styles.naviWrapper}>
        <button
          className={styles.naviButton}
          onClick={() => {
            stepChanger('before');
          }}
        >
          이전
        </button>
        {step + 1 + '/' + 2}

        <button
          className={styles.naviButton}
          onClick={() => {
            stepChanger('after');
          }}
        >
          다음
        </button>
      </div>
    </div>
  );
}

 

이제 Tutorial 페이지가 늘어나도,  GuidePopup의 코드에는 아무런 영향이 가지 않습니다.

또한 기존에는 직접 lastStep을 지정해줬는데, 배열의 길이를 통해 마지막 단계가 어딘지 구하는 로직으로 변경되었습니다.

카카오의 아티클을 보고 다형성 기반으로 개방 폐쇠 법칙도 적용하고 싶었는데, 외부에서 불러오는 데이터도 아니고 단순한 값이기에 그 정도의 깊이 있는 분리가 나오지는 않았네요..

 

커스텀 훅으로 튜토리얼 단계변경 액션 분리하기

import { useState } from 'react';

export const useStep = (lastStep: number) => {
  const [step, setStep] = useState<number>(0);
  const stepChanger = (direction: 'before' | 'after') => {
    setStep((prevStep) => {
      if (direction === 'before' && prevStep > 0) {
        return prevStep - 1;
      }
      if (direction === 'after' && prevStep < lastStep) {
        return prevStep + 1;
      }
      return prevStep;
    });
  };

  return [step, stepChanger] as const;
};

 

해당 함수의 관심사는 이전과 다음 단계로 이동시키는 것입니다.

따라서 이를 useStep이라는 훅으로 분리하고, 튜플형태로 return 시켰습니다.

https://react.dev/reference/react/useState#updating-state-based-on-the-previous-state

 

useState – React

The library for web and native user interfaces

react.dev

에서 set 함수의 경우 콜백함수 형태로 사용시 이전 상태를 사용할 수 있습니다. 따라서 이전 상태를 prevStep으로 명명하고 조건문을 얼리 리턴 기법을 사용하여 가독성을 향상시켰습니다.

 

import { useState } from 'react';

import styles from '@/app/luckyshiba/luckyshiba.module.css';
import { hideManualByDefault } from './util';
import { tutorials } from './content';
import { useStep } from './hook/useStep';

export default function GuidePopUp() {
  const lastStep = tutorials.length;
  const [step, stepChanger] = useStep(lastStep);

  return (
    <div className={styles.manualWrapper}>
      <div className={styles.manualTitle}>게임 시작 메뉴얼</div>
      <div className={styles.manualContents}>{tutorials[step]}</div>

      <button className={styles.hideButton} onClick={hideManualByDefault}>
        다시 보지 않기
      </button>
      <div className={styles.naviWrapper}>
        <button
          className={styles.naviButton}
          onClick={() => {
            stepChanger('before');
          }}
        >
          이전
        </button>
        {step + 1 + '/' + 2}

        <button
          className={styles.naviButton}
          onClick={() => {
            stepChanger('after');
          }}
        >
          다음
        </button>
      </div>
    </div>
  );
}

 

이제 마지막으로  버튼 영역을 분리하려고 합니다.

 버튼 영역 분리하기

우선 숨기기 버튼부터 분리하였습니다.

import React from 'react';
import styles from '@/app/luckyshiba/luckyshiba.module.css';
import { MANUAL_SKIP } from '@/shared/contants';
import { closeModalByEvent } from '@/shared/components/portal/util';

export default function HideButton() {
  const hideManualByDefault = () => {
    localStorage.setItem(MANUAL_SKIP, 'true');
    closeModalByEvent();
  };

  return (
    <button className={styles.hideButton} onClick={hideManualByDefault}>
      다시 보지 않기
    </button>
  );
}

기존에는 util 폴더에서 hideManualByDefault 함수를 관리하였는데, 숨기기 버튼이 분리되면서 굳이 따라 관리할 필요성을 느끼지 못했습니다. 따라서 해당 함수를 HideButton 컴퍼넌트와 결합시켰습니다.

 

이후 버튼도 page 변경용도의 버튼들이기에 Navigation이라는 이름으로 분리시켰습니다.

import React from 'react';
import styles from '@/app/luckyshiba/luckyshiba.module.css';

interface NavigationProps {
  step: number;
  stepChanger: (direction: 'before' | 'after') => void;
  totalSteps: number;
}

export default function Navigation({
  step,
  stepChanger,
  totalSteps,
}: NavigationProps) {
  return (
    <div className={styles.naviWrapper}>
      <button
        className={styles.naviButton}
        onClick={() => stepChanger('before')}
      >
        이전
      </button>
      {step + 1 + '/' + totalSteps}
      <button
        className={styles.naviButton}
        onClick={() => stepChanger('after')}
      >
        다음
      </button>
    </div>
  );
}

 

최종적으로 완성된 컴퍼넌트는

import styles from '@/app/luckyshiba/luckyshiba.module.css';
import { tutorials } from './content';
import { useStep } from './hook/useStep';
import HideButton from './control/HideButton';
import Navigation from './control/Navigation';

export default function GuidePopUp() {
  const lastStep = tutorials.length;
  const [step, stepChanger] = useStep(lastStep);

  return (
    <div className={styles.manualWrapper}>
      <div className={styles.manualTitle}>게임 시작 메뉴얼</div>
      <div className={styles.manualContents}>{tutorials[step]}</div>
      <HideButton />
      <Navigation step={step} stepChanger={stepChanger} totalSteps={lastStep} />
    </div>
  );
}

 

느낀점

원티드 프리온보딩에서 멘토님께 배운 값, 계산, 액션으로 분리하고 최근 아티클을 읽은 내용을 바탕으로 solid하게 작성하고자 하였습니다.

 

간단한 메뉴얼 컴퍼넌트였기에 SOLID패턴중에서 아티클에서 읽고 느낀 모든 내용을 반영하지는 못하였습니다.

하지만 해당 아티클에서 말하였듯이 SOLID에서 가장 핵심은 Single Responsibility Principle 이며, 관심사의 분리에 집중하여 리팩토링을 진행하였습니다.

또한 SOLID원칙을 요약하는건 간단하지면 각각의 키워드에 맞게 작성하는 과정은 좀 더 깊이 공부하고 생각할 필요성을 느꼈습니다.

아직 완벽하진 않을 수 있지만, 초기 코드에 비해서는 훨씬 가독성이 향상되었다는 점에서 보람을 느꼈습니다.

 

참고문헌

https://fe-developers.kakaoent.com/2023/230330-frontend-solid/

 

프론트엔드와 SOLID 원칙 | 카카오엔터테인먼트 FE 기술블로그

임성묵(steve) 판타지, 무협을 좋아하는 개발자입니다. 덕업일치를 위해 카카오페이지로의 이직을 결심했는데 인사팀의 실수로 백엔드에서 FE개발자로 전향하게 되었습니다. 인생소설로는 데로

fe-developers.kakaoent.com

https://www.perssondennis.com/articles/write-solid-react-hooks

 

Persson Dennis - Write SOLID React Hooks

Write SOLID React Hooks June 2, 2024 (19 minutes reading) SOLID is one of the more commonly used design patterns. It's commonly used in many languages and frameworks, and there are some articles out there how to use it in React as well. Each React article

www.perssondennis.com

 

원티드 프리온보 오종택 멘토님 강의

 

+ Recent posts