컴포넌트의 책임과 역할

김윤진·2022년 11월 25일
1

React

목록 보기
13/13

서론

리액트로 개발을 하다가 가장 하위 컴포넌트인 Button 컴포넌트를 개발하다가 해당 버튼으로 내려오는 props와 children을 클릭 핸들러 함수로 전달하기 위해 Button 컴포넌트에서 분기 처리를 하다가 Button 컴포넌트의 복잡도가 올라가는 문제를 해결 하기 위한 과정이다.


Button 컴포넌트의 책임

이 문제를 해결하기 위해서는 Button 컴포넌트의 역할에 대해 고찰해볼 필요가 있다.
Button 컴포넌트는 최하위 자식 컴포넌트로 props로 버튼의 스타일, 버튼 텍스트, 클릭 이벤트 정도를 받는다.

props를 통해 Button 컴포넌트의 역할을 보면

  • 버튼의 스타일을 통해 사용자가 어떤 버튼인지 인지시킨다.
  • 버튼을 클릭하면 클릭함수를 실행함으로 후처리 로직을 실행시킨다.
const Button = ({
  backColor = 'GREEN',
  fontColor = 'WHITE',
  onClick,
  children,
}: Props) => {
  const handleClick = () => {
    if (onClick) {
      onClick();
    }
  };

  return (
    <S.Container onClick={handleClick} backColor={backColor}>
      <S.Text fontColor={fontColor}>{children}</S.Text>
    </S.Container>
  );
};

정도로 볼 수 있다.


그런데 클릭핸들러 함수에서 해당 버튼의 텍스트나 props를 통해 부모 컴포넌트의 상태를 클릭함수에서 필요한 경우가 존재한다.
이러한 추가 사항에 대응하기 위해 props로 부모 컴포넌트의 상태를 내려주고 이 prop이 존재한다면 클릭 함수의 인자로 넣어줌으로 해결할 수 있다.

const Button = ({
  backColor = 'GREEN',
  fontColor = 'WHITE',
  value,
  onClick,
  children,
}: Props) => {
  const handleClick = () => {
    if (onClick) {
      if (value) {
        onClick(value);
        return;
      }
      onClick();
    }
  };

  return (
    <S.Container onClick={handleClick} backColor={backColor}>
      <S.Text fontColor={fontColor}>{children}</S.Text>
    </S.Container>
  );
};

여기서 2가지 문제가 발생한다.

첫번째로
이렇게 분기문을 추가함으로 Button 컴포넌트의 책임이 늘어나게 되었다.
만약 props로 value가 내려온다면 클릭 함수의 인자로 value를 넣어서 실행한다. 기존에는 단순히 클릭함수를 실행했지만 추가 요구사항으로 인해 책임이 늘어나고 추후 요구사항이 추가될 수록 이런 책임은 늘어날 수 밖에 없는 구조이다.

두번째로
타입 문제가 발생한다. 현재 props로 내려오는 onClick 함수의 타입은 onClick?: () => void; 이다. 그런데 매개변수의 타입을 넣어줘야한다. 단순히 매개변수를 옵셔널로 타입을 지정한다고 해결되는 문제가 아니다.

onClick?: (children?: string, value?: string) => void;
'(children: string, value: string) => void' 형식은
'(children?: string | undefined, value?: string | undefined) => void' 
형식에 할당할 수 없습니다.

Button 컴포넌트 props로 내려주는 children의 타입이 string이고 Button 컴포넌트에서 정의한 props에서 children의 타입은 string | undefined 타입이다.
타입스크립트는 공변성의 성질을 띄고 있어서 string | undefined 타입이 string 타입보다 타입의 범위가 넓다.
하지만 함수 매개변수에서만 반공변성을 가지고 있기 때문에 이런 에러가 발생하는 것이다.

이 문제는 두가지 방법으로 해결할 수 있다.

첫번째로
이변성으로 해결할 수 있다. onClick 함수의 매개변수의 타입이 이변성을 가질 수 있도록 한다.

onClick?: { bivarianceHack(children?: string, value?: QuizType): void }['bivarianceHack'];

이 방법의 문제점은 onClick 매개변수의 타입의 범위가 불필요하게 넓어진다는 것이다.

두번째로
두 함수를 따로 props로 내려주는 방법이 존재한다.

const Button = ({
  backColor = 'GREEN',
  fontColor = 'WHITE',
  value,
  onClick, // type : () => void
  onClickPassValue, // type: (children: string, value: string) => void;
  children,
}: Props) => {
  const handleClick = () => {
    if (onClick) {
      if (value) {
        onClickPassValue(children, value);
        return;
      }
      onClick();
    } 
  };

  return (
    <S.Container onClick={handleClick} backColor={backColor} hoverBackColor={hoverBackColor}>
      <S.Text fontColor={fontColor}>{children}</S.Text>
    </S.Container>
  );
};

이렇게 함수 매개변수의 타입 문제를 해결하여도 근본적인 문제는 해결되지 않는다.
바로 Button 컴포넌트의 책임 그대로 가지고 있기 때문이다.


책임 덜어주기

Button 컴포넌트의 책임을 덜어주기 위해 고차함수를 활용할 수 있다.
클릭함수를 선언하는 커스텀 훅에서 클릭함수를 고차함수로 선언한다.

  const onClick = (children: string, value: string) => () => {
      toggleModal();
      if (quizzes.correct_answer === children) {
        setIsCorrectAnswer(true);
        quizSolveDispatch(setCorrect(value));
      } else if (quizzes.correct_answer !== children) {
        setIsCorrectAnswer(false);
        quizSolveDispatch(setWrong(value));
      }
};

이로 인해 children과 value의 전달은 Button 컴포넌트의 부모 컴포넌트에서 전달하게 된다.
그리고 Button 컴포넌트에서의 실행은

() => {
      toggleModal();
      if (quizzes.correct_answer === children) {
        setIsCorrectAnswer(true);
        quizSolveDispatch(setCorrect(value));
      } else if (quizzes.correct_answer !== children) {
        setIsCorrectAnswer(false);
        quizSolveDispatch(setWrong(value));
      }
};

이 함수가 실행이 되는 것이다.


children, value, 매개변수는 부모 컴포넌트에서 전달하는 것이다.

  • Button 컴포넌트의 부모 컴포넌트
 <>
  {allAnswers.map((incorrectAnswer) => (
    <Button
      key={incorrectAnswer}
      onClick={handleClickQuizAnswer(incorrectAnswer, quizzes[quizCount])}
    >
      {incorrectAnswer}
    </Button>
  ))}
</>
  • Button 컴포넌트
const Button = ({
  backColor = 'GREEN',
  fontColor = 'WHITE',
  onClick,
  children,
}: Props) => {
  const handleClick = () => {
    if (onClick) {
      onClick();
    }
  };

  return (
    <S.Container onClick={handleClick} backColor={backColor}>
      <S.Text fontColor={fontColor}>{children}</S.Text>
    </S.Container>
  );
};

고차함수를 통해 가져갈 수 있는 장점은

  • Button 컴포넌트에서의 불필요한 책임을 줄일 수 있다.
  • 추후 value or children만 필요하거나 다른 값이 필요한 경우에 Button 컴포넌트 로직을 변경하는 것이 아니기에 쉽게 대응할 수 있다.
  • Button 컴포넌트의 복잡성을 줄일 수 있다.

마무리

단순히 Button 컴포넌트의 복잡도를 줄이기 위함이 아닌 컴포넌트의 역할을 생각해보고 해당 컴포넌트가 불필요한 책임을 지고 있지는 않는가에 대해 다시 한번 생각해보고 추후에도 이 책임과 역할을 고려하면서 프로그래밍을 할 수 있는, 새로운 관점으로 바라볼 수 있게된 좋은 기회였다.
이런 새로운 관점에서 바로볼 수 있게 기회를 제공한 시지프에게 감사함을 더한다.!

0개의 댓글