실무에서 바로 쓰는 Frontend Clean Code 정리

dev_hee·2022년 8월 11일
9

FE tips

목록 보기
1/5

본문은 토스 SLASH 21 - 실무에서 바로 쓰는 Frontend Clean Code 영상을 보고 정리한 글입니다. 모든 내용은 해당 영상에 포함되어 있습니다.


클린코드

클린코드는 '명확한 이름', '중복 줄이기'와 섬세하게 코드를 정리하는 스킬을 요구합니다.

실무에서 클린 코드의 의의

지뢰코드는 흐름 파악이 어렵고, 도메인 맥락 표현이 안되어 동료에게 물어봐야 알 수 있는 코드입니다.

지뢰코드의 특징은 다음과 같습니다.

  • 유지보수할 때 시간이 오래걸립니다.
  • 기능 추가가 불가능할 수 있습니다.
  • 성능도 안좋은 경우가 많다.

실무에서 클린코드의 의의는 유지보수 시간의 단축을 의미합니다.
동료나 내가 과거에 작성한 코드를 빠르게 이해할 수 있다면 유지보수할 때 드는 시간이 짧아집니다.

클린한 코드의 장점은 다음과 같습니다.

  • 유지보수 시간이 단축된다.
  • 코드 파악이 쉽다.
  • 디버깅이 쉽다.
  • 코드리뷰의 시간도 단축된다.

안일한 코드 추가의 함정

예를 들어봅시다. 기존 서비스에 기능 추가 요청이 들어왔습니다.
보험에 대한 질문을 입력하는 페이지가 있는데 설계사가 있는 경우에는 설계사 사진이 들어간 팝업을 먼저 띄워달라는 기능 추가입니다.

기존 코드는 다음과 같습니다.

기존 코드

function QuestionPage() {
  async function handleQuestionSubmit() {
    const 약관동의 = await 약관동의_받아오기();
    if (!약관동의) {
      await 약관동의_팝업열기();
    }
    await 질문전송(questionValue);
    alert('질문이 등록되었어요');
  }
  return (
    <main>
      <form>
        <textarea placeholder="어떤 내용이 궁금한가요?" />
        <Button onClick={handleQuestionSubmit}>질문하기</Button>
      </form>
    </main>
  );
}

간단하게 코드 구조를 생각하지 않고 기능 구현을 하면 다음과 같습니다.

간단한 구현 설계 (지뢰코드)

function QuestionPage() {
  const [popupOpened, setPopupOpened] = useState(false); // 팝업 상태

  async function handleQuestionSubmit() {
    const 연결전문가 = await 연결전문가_받아오기();
    if (연결전문가 !== null) {
      setPopupOpened(true);
    } else {
      const 약관동의 = await 약관동의_받아오기();
      if (!약관동의) {
        await 약관동의_팝업열기();
      }
    }
    await 질문전송(questionValue);
    alert('질문이 등록되었어요');
  }

  async function handleMyExpertQuestionSubmit() {
    await 연결전문가_질문전송(questionValue, 연결전문가.id);
    alert(`${연결전문가.namge}에게 질문이 등록되었어요.`);
  }

  return (
    <main>
      <form>
        <textarea placeholder="어떤 내용이 궁금한가요?" />
        <Button onClick={handleQuestionSubmit}>질문하기</Button>
      </form>
      {popupOpened && (
        <연결전문가팝업 onSubmit={handleMyExpertQuestionSubmit} />
      )}
    </main>
  );
}

하지만 기능이 추가되면서 나쁜 코드가 되었습니다.

그 이유는 다음과 같습니다.

  • 하나의 목적인 코드가 흩뿌려져있다. (연결적인 전문가 팝업 관련 코드가 분할되어있다.)
  • 하나의 함수가 여러가지 일을 하고있다. (handleQuestionSubmit 함수는 총 3가지 일을 하고있다.) 따라서 세부 구현을 모두 읽어야 함수의 역할을 알 수 있다.
  • 함수의 세부구현 단계가 제각각이다. (handleQuestionSubmit, handleMyExpertQuestionSubmit는 이벤트 핸들러이지만 handleQuestionSubmit이 질문전송 외에도 여러가지 일을 하고있다.)

처음에 구현되어 있던 코드는 그 당시에는 옳은 구조였지만, 작은 기능을 추가하고 나서는 지금은 어지럽고 더러운 코드가 되었습니다.

코드 리팩토링

  1. 함수 세부 구현 단계 통일

    handleQuestionSubmithandleMyExpertQuestionSubmit 의 함수 세부 구현 단계를 통일했습니다.

    handleQuestionSubmithandleNewExpertQuestionSubmit 로 이름을 수정하고, 내부에서 하던 3가지 일을 제거하고 한 가지 일만 수행하도록 만들었습니다.

    async function handleNewExpertQuestionSubmit() {
        // 새로운 전문가에게 질문하는 로직만 존재
        await 질문전송(questionValue);
        alert('질문이 등록되었어요');
    }
    
    async function handleMyExpertQuestionSubmit() {
        // 연결 중인 전문가에게 질문하는 로직만
        await 연결전문가_질문전송(questionValue, 연결전문가.id);
        alert(`${연결전문가.namge}에게 질문이 등록되었어요.`);
    }
  2. 목적이 동일한 코드는 뭉쳐두기

    기존에는 팝업을 여는 버튼과 팝업 코드가 동떨어져 있었는데, 이를 모아서 PopupTriggerButton 컴포넌트를 만들고 띄울 팝업은 props로 전달해 주었습니다.

    return (
        <main>
        <form>
            <textarea placeholder="어떤 내용이 궁금한가요?" />
            {연결전문가.connected ? (
            <PopupTriggerButton
                popup={
                <연결전문가팝업 onButtonSubmit={handleMyExpertQuestionSubmit} />
                }
            >
                질문하기
            </PopupTriggerButton>
            ) : (
  3. 함수가 한 가지 일만 하도록 쪼개기

    약관 동의 함수(openPopupToNotAgreedUsers)를 쪼개서 필요한 시점에 부르도록 바꿨습니다.

    async function openPopupToNotAgreedUsers() {
        // 약관 동의 함수
        const 약관동의 = await 약관동의_받아오기();
        if (!약관동의) {
        await 약관동의_팝업열기();
        }
    }
    
    return (
        <main>
        <form>
            // ...
            ) : (
            <Button
                onClick={async () => {
                await openPopupToNotAgreedUsers();
                await handleNewExpertQuestionSubmit();
                }}
            >
                질문하기
            </Button>
            )}
        </form>
        </main>
    );

코드는 첫번째 버전보다 길어졌습니다.
짧은 코드가 클린 코드가 아니기 때문입니다.

클린코드 != 짧은코드
클린코드 == 원하는 로직을 빠르게 찾을 수 있는 코드

클린코드는 짧은 코드가 아니라, 원하는 로직을 빠르게 찾을 수 있는 코드이기 때문입니다.

로직을 빠르게 찾을 수 있는 코드

원하는 로직을 빠르게 찾으려면 다음과 같은 특징을 잘 살려야 합니다.

  1. 응집도
    하나의 목적을 가진 코드가 흩뿌려져 있을 때, 응집도를 높여 뭉쳐두어야 합니다.

  2. 단일책임
    함수가 여러 가지 일을 하고있다면 단일 책임 원칙에 의해 쪼개주어야 합니다.

  3. 추상화
    함수의 세부구혐 단계가 제각각 일때에는 추상화 단계를 조정해서 핵심 개념을 필요한 만큼만 노출해야 합니다.

응집도

다음 코드 예시는 팝업을 조작하는 코드가 흩뿌려져 있습니다.
파악도 한번에 안되고 버그가 발생할 위험이 높은 코드입니다.

function QuestionPage() {
  const [popupOpened, setPopupOpened] = useState(false); // 팝업

  function handleClick() {
    setPopupOpened(true);
  }

  async function handleQuestionSubmit() { // 팝업
    await 질문전송(연결전문가.id);
    alert('질문을 전송했습니다.');
  }

  return (
    <main>
      <button onClick={handleClick}>질문하기</button>
      <Popup title="보험 질문하기" open={popupOpened}> // 팝업
        <div>전문가가 설명해드려요</div>
        <button onClick={handlePopupSubmit}>확인</button>
      </Popup>
    </main>
  );
}

1. 리팩토링 v1

커스텀 훅을 사용해서 한 군데로 뭉쳐보았습니다.
이제 openPopup 만 호출하면 팝업을 열 수 있게 되었습니다.

하지만 이는 오히려 읽기 힘든 코드가 되었습니다.
어떤 내용의 팝업을 띄우는지, 그리고 팝업에서 버튼을 눌렀을 때 어떤 액션을 하는지가 여기서 가장 중요한 포인트인데 hook 속에 감춰져 알 수 없게 되었습니다.

function QuestionPage() {
  const [openPopup] = useMyExpertPopup(연결전문가.id);

  function handleClick() {
    openPopup();
  }
  return <button onClick={handleClick}>질문하기</button>;
}

그렇다면 무엇을 뭉쳐야 할까요?

뭉쳐야 하는 것뭉치면 안되는 것
당장 몰라도 되는 디테일코드 파악에 필수적인 핵심 정보
짧은 코드만 보고도 빠르게 코드의 목적을 파악할 수 있다.여러 모듈을 넘나들며 흐름을 따라가야하는 대참사가 발생한다.

뭉쳐서 짧은 코드를 만든다고 깨끗한 코드가 되는 것은 아닙니다.
클린 코드는 짧은 코드가 아니라, 찾고 싶은 코드를 빠르게 찾을 수 있는 코드 입니다.

2. 리팩토링 v2

코드 응집 Tip: 핵심 데이터와 세부 구현 나누기

남겨야할 핵심 데이터와 숨겨도 될 세부 구현을 나눠봅시다.

  • 핵심 데이터 : 팝업 버튼 클릭시 액션, 팝업 제목과 내용
  • 세부 구현 : 팝업을 열고 닫을 때 사용하는 상태, 컴포넌트 마크업, 핸들러 바인딩
function QuestionPage() {
  const [openPopup] = usePopup();

  async function handleClick() {
    /** 팝업 제목, 내용 */
    const confirmed = await openPopup({
      title: '보험 질문하기',
      content: <div>전문가가 설명해드려요</div>,
    });
    if (confirmed) {
      /** 팝업 버튼 클릭 action */
      await submitQuestion();
    }
  }

  async function submitQuestion(연결전문가) {
    await 질문전송(연결전문가.id);
    alert('질문을 전송했습니다.');
  }

  return <button onClick={handleClick}>질문하기</button>;
}

세부 구현(usePopup)은 읽지 않고도 어떤 팝업인지 파악할 수 있다.

선언적 프로그래밍

핵심 데이터만 전달받고 세부 구현은 뭉쳐 숨겨 두는 개발 스타일을 선언적 프로그래밍이라고 부른다.

<Popup onSubmit={질문전송} onSuccess={홈으로이동} />;
  • 무엇을 해야할지만 알려주면 빠르게 코드 파악이 가능합니다. (질문전송, 홈으로이동)
  • 세부 구현은 안쪽에 뭉쳐두어 신경 쓸 필요가 없습니다. (onSubmit, onSuccess)
  • 무엇만 바꿔서 쉽게 재사용할 수 있습니다. (질문전송, 홈으로이동)

선언적 프로그래밍도 내부에 세부 구현은 명령형으로 작성되어 있습니다. 명령형은 어떻게 해야 할지 하나하나 명령하는 방식입니다. 명령형은 세부 구현이 모두 노출되어있어 커스텀하기 쉽지만 읽는데 오래걸리고 재사용하기 어렵습니다.

단일책임

하나의 일을 하는 뚜렷한 이름의 함수를 만들어야 합니다.

다음 함수는 이름은 handle질문제출 이지만, 약관 체크 후 팝업질문 제출 두 가지 일을 수행합니다.

  async function handle질문제출() {
    /* 약관 체크 후 팝업 */
    const 약관동의 = await 약관동의_받아오기();
    if (!약관동의) {
      await 약관동의_팝업열기();
    }
    /* 질문 제출 */
    await 질문전송(questionValue);
    alert('질문이 등록되었어요');
  }

이렇게 중요 포인트가 모두 담겨있지 않은 함수명은 읽는 이가 예상한대로 코드가 동작하지 않으며, 이는 코드에 대한 신뢰 하락으로 이어집니다.

여기서 기능 추가가 들어가면 함수는 더욱 복잡해집니다.
이러한 기능 추가가 반복될 시, 지뢰코드가 됩니다.

리팩토링 Tip 1. 함 가지 일만 하는, 명확한 이름의 함수

다음처럼 한 가지 일을 하는 함수로 쪼갤 수 있습니다.

  async function handle새전문가질문제출() {
    // 새로운 전문가에게 질문하는 로직만 존재
    await 질문전송(questionValue);
    alert('질문이 등록되었어요');
  }

  async function handle연결전문가질문제출() {
    // 연결 중인 전문가에게 질문하는 로직만
    await 연결전문가_질문전송(questionValue, 연결전문가.id);
    alert(`${연결전문가.namge}에게 질문이 등록되었어요.`);
  }

  async function handle약관동의팝업() {
    // 약관 동의 함수
    const 약관동의 = await 약관동의_받아오기();
    if (!약관동의) {
      await 약관동의_팝업열기();
    }
  }

리팩토링 Tip 2. 함 가지 일만 하는, 기능성 컴포넌트

  • before

버튼 클릭 함수에 로그 찍는 함수와 API 호출이 섞여있습니다.

<button
  onClick={async () => {
    log('제출 버튼 클릭');
    await openConfirm();
  }}
/>
  • after

로그는 버튼을 감싼 컴포넌트에서 찍고, 버튼 클릭 함수에는 API 호출만 신경씁니다.

<LogClick message="제출 버튼 클릭">
  <button onClick={openConfirm} />;
</LogClick>

리팩토링 Tip 3. 조건이 많아지면 한글 이름도 유용해요

이름 짓기가 복잡하면 한글 변수명을 사용하는 것도 유용합니다.
도메인이 복잡해서 영어이름이 길어져서 오히려 복잡도가 높아질 때, 상수를 직관적으로 보고싶을 때, 복잡한 조건문이 많아질 때 유용합니다.
마치 주석을 달아둔 것 같은 효과를 냅니다.

추상화

로직에서 핵심 개념을 뽑아내는 것이 추상화 입니다.

  • 피카소의 '소' 추상화

디테일을 점점 지워나가다가 소에서 가장 남기고 싶은 개념만 남겼습니다.

  • 지하철 노선도의 추상화

현실 세계의 지하철 구조는 복잡하지만, 지도 위의 색색의 선으로 추상화 합니다.
더욱 간단하게 추상화 하여 역 이름과 호선만 남기고 모두 숨깁니다.

프론트엔드 코드의 추상화

  1. 컴포넌트 를 통한 추상화

    추상화를 하지 않은 코드

        <div>
        <button
            onClick={async () => {
            const res = await 회원가입();
            if (res.success) {
                프로필로이동();
            }
            }}
        >
            전송
        </button>
        </div>

    중요 개념만 남기고 추상화

    <Popup onSubmit={회원가입} onSuccess={프로필로이동} />
  2. 함수 를 통한 추상화

    • 코드 세부 구현
    const planner = await fetchPlanner(plannerId);
    const label = planner.new ? '새로운 상담사' : '연결중인 상담사';
    • 중요 개념을 함수 이름에 담아 추상화
    const label = await getPlannerLabel(plannerId);

얼마나 추상화할 것인가?

구체적인 코드를 조금 추상적이게, 더욱 추상적이게 리팩토링할 수 있습니다.
필요에 따라 핵심 기능을 밖으로 노출하며 추상 레벨을 선택할 수 있습니다.

  • level 0
<button onClick={handleClick}>전송</button>
{isShowConfirm && (
  <Confirm
    onClick={() => {
      showMessage('성공');
    }}
  />
)}
  • level 1
<ConfirmButton
  onConfirm={() => {
    showMessage('성공');
  }}
>
  전송
</ConfirmButton>;
  • level 2
<ConfirmButton message="성공">전송</ConfirmButton>;
  • level 3
<ConfirmButton />

주의! 추상화 레벨이 섞여있으면 코드 파악이 어렵습니다.

추상화 레벨이 섞여있으면 전체 코드가 어느 수준으로 구체적으로 기술된지 파악하기 어렵습니다. 작은 코드라고 생각했는데 복잡한 코드가 나오게 되면 코드를 읽는데 사고가 널뛰게 됩니다.

추상화 단계를 비슷하게 정리하면 물흐르듯 읽을 수 있습니다.

액션 아이템

  1. 담대하게 기존코드 수정하기

    두려워하지 말고 기존 코드를 씹고 뜯고 맛보고 즐기며 수정합니다.

    PR 에 file changes가 많이 남는 것이 두렵다면, mother branch를 따서 리팩토링한 PR을 추가로 만드는 방법을 사용해봅시다.

  2. 큰 그림 보는 연습하기

    그 때는 지금은 틀릴 수 있습니다. 기능 추가 자체는 클린해도 전체적으로는 어지러울 수 있습니다.

  3. 팀과 함께 공감대 형성하기

    코드에 정답은 없기 때문에 명시적으로 이야기 하는 시간을 가지는 것이 필요합니다.

  4. 문서로 적어보기

    클린 코드는 모호한 개념입니다. 글로 적어야 명확해집니다. 이 코드가 향후 어떤 점에서 위험할 수 있는지, 어떻게 개선할 수 있는지 정리할 수 있습니다.

profile
🎨그림을 좋아하는 FE 개발자👩🏻‍💻

0개의 댓글