컴포넌트 합성을 통한 관심사 분리 및 중복된 코드 제거

김윤진·2022년 9월 28일
1

React

목록 보기
11/13

레벨로그 프로젝트

리펙터링을 진행하면서 항상 느끼는 것이지만 좋은 리펙터링의 결과는 불편함에서 나온다고 생각한다. 코드에 대한 불편함을 느껴야 리펙터링을 하는 의미를 느낄 수 있고 가독성 좋은 코드가 탄생하는 것이다.

리펙터링 전

버튼의 클릭 이벤트 리스너에 handle...함수를 달아주어서 버튼을 클릭하면 해당하는 uri로 navigate한다. 이 코드에서 불편함을 느낄 수 있다. 커스텀 훅을 통해서 비즈니스 로직과 뷰 로직을 분리하는 것을 목표로 프로젝트를 진행하고 있다. 그래서 컴포넌트 단에서 필요한 비즈니스 로직을 커스텀 훅에 이관하면 쉽게 끝날 것 같지만 한 가지 문제가 있다.
바로 navigate하는 uri가 컴포넌트 props로 내려 받은 데이터에 의존하고 있다는 것이다. 그렇게 되면 커스텀 훅에서 해당하는 handler함수를 정의할 수 없다는 것이다. 그렇다면 navigate만 하는 handler함수는 없앨 수 있지 않을까? 라는 생각이 들었다.
그러면 여기서 목표를 정해보자. 목표는 컴포넌트 단에서 navigate만 하는 handle...함수를 정의하지 않는 것이다.

const Interviewer = ({ participant }) => { 
  
  const handleClickOpenLevellogModal = () => {
    if (typeof teamId === 'string') {
      onClickOpenLevellogModal({ teamId, participant });
    }
  };

  const handleClickOpenPreQuestionModal = () => {
    onClickOpenPreQuestionModal({ participant });
  };

  const handleClickAddPreQuestionButton = () => {
      navigate(preQuestionAddUriBuilder({ teamId, levellogId: participant.levellogId }));
    };

  const handleClickViewFeedbackButton = () => {
      navigate(feedbacksGetUriBuilder({ teamId, levellogId: participant.levellogId }));
    };

  const handleClickViewInterviewQuestionButton = () => {
      navigate(interviewQuestionsGetUriBuilder({ teamId, levellogId: participant.levellogId }));
    };


   return (
       <S.Container>
      {role.interviewee && role.interviewer === false && <Role role={'인터뷰이'} />}
      {role.interviewer && role.interviewee === false && <Role role={'인터뷰어'} />}
      {role.interviewee && role.interviewer && <Role role={'상호 인터뷰'} />}
      <S.Profile>
        <Image
          src={participant.profileUrl}
          sizes={'HUGE'}
          githubAvatarSize={GITHUB_AVATAR_SIZE_LIST.HUGE}
        />
        <S.NicknameBox>
          <S.Nickname>{participant.nickname}</S.Nickname>
        </S.NicknameBox>
      </S.Profile>
      <S.Content>
        <S.ButtonBox>
            <S.Button disabled={!participant.levellogId} onClick={handleClickOpenLevellogModal}>
              <Image src={levellogIcon} sizes={'SMALL'} borderRadius={false} />
              <S.ButtonText>레벨로그 보기</S.ButtonText>
            </S.Button>

            <CustomLink
              path={levellogAddUriBuilder({ teamId })}
              disabled={teamStatus !== TEAM_STATUS.READY}
            >
              <S.Button disabled={teamStatus !== TEAM_STATUS.READY} onClick={handleClickOpenLevellogModal}>
                <Image src={levellogIcon} sizes={'SMALL'} borderRadius={false} />
                <S.ButtonText>레벨로그 작성</S.ButtonText>
              </S.Button>
            </CustomLink>

              <S.Button disabled={!participant.levellogId || !userInTeam} onClick={handleClickViewInterviewQuestionButton}>
                <Image src={interviewQuestionIcon} sizes={'SMALL'} borderRadius={false} />
                <S.ButtonText>인터뷰질문 보기</S.ButtonText>
              </S.Button>

            <S.Button
              disabled={!participant.levellogId || !userInTeam}
              onClick={handleClickOpenPreQuestionModal}
            >
              <Image src={preQuestionIcon} sizes={'SMALL'} borderRadius={false} />
              <S.ButtonText>사전질문 보기</S.ButtonText>
            </S.Button>

              <S.Button disabled={!participant.levellogId || !userInTeam} onClick={handleClickAddPreQuestionButton}>
                <Image src={preQuestionIcon} sizes={'SMALL'} borderRadius={false} />
                <S.ButtonText>사전질문 작성</S.ButtonText>
              </S.Button>

            <S.Button disabled={!participant.levellogId || !userInTeam} onClick={handleClickViewFeedbackButton}>
              <Image src={feedbackIcon} sizes={'SMALL'} borderRadius={false} />
              <S.ButtonText>피드백 작성 / 보기</S.ButtonText>
            </S.Button>
        </S.ButtonBox>
      </S.Content>
    </S.Container>
    );
  };
}

리펙터링 후

handler.. 함수 내부에서 navigate하는 함수를 React의 Link 태그로 대신하였다. 그리고 Link 태그를 감싸주는 CustonLink라는 컴포넌트를 만들었다. CustonLink 컴포넌트의 역할은 Button이 disabled라면 Link 태그 없이 Button을 보여주고 disabled가 아니라면 CustonLink 컴포넌트로 Button을 감싸서 보여준다. 왜 CustomLink 컴포넌트로 Button 컴포넌트를 감싸주냐?
Button 컴포넌트 내부의 a 태그를 넣으면 Button 컴포넌트가 disabled여도 Button 컴포넌트의 disabled가 동작하지 않는다. Link 태그도 결국 a 태그로 이루어져있기 때문에 동일한 문제가 발생한다. 그래서 버튼의 disabled 여부에 따라 Link를 감싸는 여부도 결정되는 것이다. 그리고 Link 태그를 사용함으로써 불필요한 handler.. 함수도 뷰 컴포넌트에서 제거할 수 있다.

  • CustomLink 컴포넌트
import { Link } from 'react-router-dom';

const CustomLink = ({ path, disabled, children }: CustomLinkProps) => {
  if (disabled) {
    return <>{children}</>;
  }

  return <Link to={path}>{children}</Link>;
};

interface CustomLinkProps {
  path: string;
  disabled: boolean;
  children: JSX.Element;
}

export default CustomLink;
  • Interviewer 컴포넌트
const Interviewer = ({ participant }) => { 
    const handleClickOpenLevellogModal = () => {
    if (typeof teamId === 'string') {
      onClickOpenLevellogModal({ teamId, participant });
    }
  };

  const handleClickOpenPreQuestionModal = () => {
    onClickOpenPreQuestionModal({ participant });
  };
  
  return (
       <S.Container>
      {role.interviewee && role.interviewer === false && <Role role={'인터뷰이'} />}
      {role.interviewer && role.interviewee === false && <Role role={'인터뷰어'} />}
      {role.interviewee && role.interviewer && <Role role={'상호 인터뷰'} />}
      <S.Profile>
        <Image
          src={participant.profileUrl}
          sizes={'HUGE'}
          githubAvatarSize={GITHUB_AVATAR_SIZE_LIST.HUGE}
        />
        <S.NicknameBox>
          <S.Nickname>{participant.nickname}</S.Nickname>
        </S.NicknameBox>
      </S.Profile>
      <S.Content>
        <S.ButtonBox>
            <S.Button disabled={!participant.levellogId} onClick={handleClickOpenLevellogModal}>
              <Image src={levellogIcon} sizes={'SMALL'} borderRadius={false} />
              <S.ButtonText>레벨로그 보기</S.ButtonText>
            </S.Button>

            <CustomLink
              path={levellogAddUriBuilder({ teamId })}
              disabled={teamStatus !== TEAM_STATUS.READY}
            >
              <S.Button disabled={teamStatus !== TEAM_STATUS.READY}>
                <Image src={levellogIcon} sizes={'SMALL'} borderRadius={false} />
                <S.ButtonText>레벨로그 작성</S.ButtonText>
              </S.Button>
            </CustomLink>

            <CustomLink
              path={interviewQuestionsGetUriBuilder({ teamId, levellogId: participant.levellogId })}
              disabled={!participant.levellogId || !userInTeam}
            >
              <S.Button disabled={!participant.levellogId || !userInTeam}>
                <Image src={interviewQuestionIcon} sizes={'SMALL'} borderRadius={false} />
                <S.ButtonText>인터뷰질문 보기</S.ButtonText>
              </S.Button>
            </CustomLink>

            <S.Button
              disabled={!participant.levellogId || !userInTeam}
              onClick={handleClickOpenPreQuestionModal}
            >
              <Image src={preQuestionIcon} sizes={'SMALL'} borderRadius={false} />
              <S.ButtonText>사전질문 보기</S.ButtonText>
            </S.Button>

            <CustomLink
              path={preQuestionAddUriBuilder({ teamId, levellogId: participant.levellogId })}
              disabled={!participant.levellogId || !userInTeam}
            >
              <S.Button disabled={!participant.levellogId || !userInTeam}>
                <Image src={preQuestionIcon} sizes={'SMALL'} borderRadius={false} />
                <S.ButtonText>사전질문 작성</S.ButtonText>
              </S.Button>
            </CustomLink>

          <CustomLink
            path={feedbacksGetUriBuilder({ teamId, levellogId: participant.levellogId })}
            disabled={!participant.levellogId || !userInTeam}
          >
            <S.Button disabled={!participant.levellogId || !userInTeam}>
              <Image src={feedbackIcon} sizes={'SMALL'} borderRadius={false} />
              <S.ButtonText>피드백 작성 / 보기</S.ButtonText>
            </S.Button>
          </CustomLink>
        </S.ButtonBox>
      </S.Content>
    </S.Container>
    );
}

0개의 댓글