[React] 실무에서의 React Clean Code

2026-02-14 hit count image

Toss의 SLASH 21 컨퍼런스에서 발표된 React Clean Code의 핵심 개념인 응집도, 단일 책임, 추상화를 실제 코드 예제와 함께 정리해서 공유합니다.

react

개요

Toss라는 한국의 핀테크 회사에서 SLASH라는 컨퍼런스에서 발표된 영상의 내용이 좋아서 공유합니다.

Toss는 한국의 핀테크 스타트업 회사로, 현재는 상장하여 한국인 개발자들이 가고 싶어하는 회사 상위에 있는 회사입니다.

Clean Code라고 하면 알기 쉬운 이름, 중복을 줄이는 것 등을 떠올리지만, 실무에서는 더 읽기 쉬운 코드를 위한 스킬이 필요합니다.

여기서는 프론트엔드에서 읽기 쉬운 코드를 작성하기 위한 개념과 액션 아이템을 공유합니다.

예제 코드는 React입니다. 실제 동작하는 코드가 아니므로, 개념 파악에 중점을 두면 좋을 거 같습니다..

실무에서의 Clean Code 정의

개발자의 자기만족 외에, 실무에서 Clean Code를 하는 의미는 무엇일까요?

그 코드는 안 건드리는 게 좋을 것 같아요.

일단 제가 수정할게요.

실무에서 이런 말을 들어본 적이 있을 겁니다. 대부분의 회사에서는 이런 지뢰 코드를 가지고 있습니다.

이 말에는 다음과 같은 의미가 담겨 있습니다.

  1. 코드 흐름의 의미 파악이 어렵다
  2. 도메인 맥락이 잘 표현되지 않았다
  3. 다른 멤버에게 물어봐야 알 수 있는 코드다

이 지뢰 코드는 개발할 때 병목이 되고, 유지보수할 때 코드 파악에 오랜 시간이 걸립니다.

최악의 경우, 기능 추가가 불가능할 수 있습니다.

또한 이런 코드는 성능도 좋지 않아서, 사용자 입장에서도 좋지 않은 경험을 하게 되는 경우가 많습니다.

실무에서의 Clean Code의 의미 = 유지보수 시간의 단축

실무에서 Clean Code는 유지보수 시간의 단축을 의미합니다.

다른 멤버 또는 과거의 내가 작성한 코드를 빠르게 이해할 수 있으면, 유지보수할 때 개발 시간이 짧아집니다.

유지보수 시간의 단축 = 코드 파악, 디버깅, 리뷰 시간 단축

읽기 쉬운 깔끔한 코드는 코드 리뷰 시간도, 버그가 발생했을 때 디버깅 시간도 단축해줍니다.

시간 = 리소스 = 돈

시간은 리소스이고, 리소스는 돈과 같습니다. 고치는 데 1일 걸리는 코드와 3일 걸리는 코드가 있다면, 간단하게 계산해서 3일 걸리는 코드는 개발자, 또는 개발자의 노력이 3배 필요합니다. 즉, 개발자인 우리가 3배 더 일을 해야 합니다.

코드 추가의 함정

우리는 프로 개발자이므로, 처음 코드를 설계하고 만들 때는 매우 깔끔한 코드를 작성할 수 있습니다.

하지만 기존 코드에 기능을 추가하는 상황에서는 이야기가 달라집니다. 조금이라도 긴장을 놓으면 코드가 나빠집니다.

우리 업무의 90%가 기존 코드에 기능을 추가하는 것입니다. 다른 멤버가 작성한 코드, 지난주 내가 작성한 코드에 기능을 넣습니다.

발표자가 실제 받은 업무는 다음과 같습니다.

보험에 관해 질문을 입력하는 페이지가 있었는데, 사용자의 보험 담당자가 있는 경우 그 담당자의 사진이 들어간 팝업을 표시해달라는 기능 추가입니다.

설계와 구현

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

function QuestionPage() {
  async function handleQuestionSubmit() {
    // 이용약관 확인
    const agree = await getTargetElement();
    if (!agree) {
      // 이용약관이 필요한 경우, 팝업 표시
      await openAgreePopup();
    }
    // 이용약관에 동의한 사용자의 경우, 질문을 등록하고 성공 메시지 표시
    await sendQuestion(questionValue);
    alert('질문이 등록되었습니다.');
  }

  return (
    <Main>
      <form>
        <textarea placeholder="질문 내용을 입력해주세요" />
        <button onClick={handleQuestionSubmit}>질문 등록</button>
      </form>
    </Main>
  );
}

이 코드에 새로운 기능을 추가하려면 어떻게 하면 될까요? 간단하게 다음과 같이 추가하면 될 것 같습니다.

【설계】

function QuestionPage() {
  async function handleQuestionSubmit() {
    /*
     * 【보험 담당자가 있는 경우를 체크하고,
     *   담당자가 있는 경우 팝업을 표시하는 로직 추가】
     */
    // 이용약관 확인
    const agree = await getTargetElement();
    if (!agree) {
      // 이용약관이 필요한 경우, 팝업 표시
      await openAgreePopup();
    }
    // 이용약관에 동의한 사용자의 경우, 질문을 등록하고 성공 메시지 표시
    await sendQuestion(questionValue);
    alert('질문이 등록되었습니다.');
  }

  return (
    <Main>
      <form>
        <textarea placeholder="질문 내용을 입력해주세요" />
        <button onClick={handleQuestionSubmit}>질문 등록</button>
        /* * 【팝업 컴포넌트 추가. 처음에는 숨기고, 필요할 때 표시】 */
      </form>
    </Main>
  );
}

【개발】

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

  async function handleQuestionSubmit() {
    // 담당자가 있는 경우, 팝업 표시
    const myExpert = await getMyExpert();
    if (myExpert !== null) {
      setPopupOpened(true);
    } else {
      // 이용약관 확인
      const agree = await getTargetElement();
      if (!agree) {
        // 이용약관이 필요한 경우, 팝업 표시
        await openAgreePopup();
      }
      // 이용약관에 동의한 사용자의 경우, 질문을 등록하고 성공 메시지 표시
      await sendQuestion(questionValue);
      alert('질문이 등록되었습니다.');
    }
  }

  async function handleMyExpertQuestionSubmit() {
    await sendQuestionToMyExpert(questionValue, expert.id);
    alert(`${myExpert.name}에게 질문했습니다.`);
  }

  return (
    <Main>
      <form>
        <textarea placeholder="질문 내용을 입력해주세요" />
        <button onClick={handleQuestionSubmit}>질문 등록</button>
        {popupOpened && (
          <MyExpertPopup onSubmit={handleMyExpertQuestionSubmit} />
        )}
      </form>
    </Main>
  );
}

설계대로 개발했습니다.

팝업이 표시되었는지 여부를 저장하는 State를 추가하고, 기존 클릭 함수에 보험 담당자가 있는지 확인하는 if문을 추가했습니다.

보험 담당자 팝업에서 확인 버튼을 눌렀을 때 호출되는 함수를 만들고, 팝업 컴포넌트도 추가했습니다.

문제점

이것은 올바르고 당연한 코드 추가처럼 보이지만, 실은 나쁜 코드가 되었습니다.

  1. 하나의 목적을 가진 코드가 여기저기 흩어져 있습니다. 아래 코드는 보험 담당자와 팝업과 관련된 코드인데, 이 코드가 떨어져 있어서 나중에 기능을 추가할 때 스크롤하면서 여기저기 확인해야 합니다.

    1. popupOpened State
    2. 담당자 체크, 팝업 표시 로직
    3. 담당자에게 데이터를 보내는 함수
    4. 담당자 팝업 컴포넌트
  2. 하나의 함수가 여러 역할을 하고 있습니다. 기존 함수가 3개의 역할을 하고 있습니다. 함수 내용을 전부 읽지 않으면 함수의 역할을 파악할 수 없습니다. 그래서 코드를 추가하거나 삭제할 때도 더 많은 시간이 걸립니다.

  3. 함수의 상세 구현 계층이 다릅니다. handleQuestionSubmit 함수와 handleMyExpertQuestionSubmit 함수는 이벤트 핸들링 함수입니다. 이름은 handleQuestionSubmithandleMyExpertQuestionSubmit으로 비슷하지만, handleQuestionSubmit 함수는 질문 등록 외에도 여러 가지를 하고 있어서, 코드를 전반적으로 보지 않으면 이해하기 어렵습니다. 이름만 보고 코드의 내용을 예측할 수 없게 되었으므로, 전부 확인하거나 이름만 보고 잘못 이해할 가능성이 생깁니다.

처음 코드는 좋았는데, 단 하나의 기능 추가로 이해하기 어려운 코드가 되었습니다.

여기서의 함정은 PR에서 보면, 이것이 읽기 어려운 코드라고 인식하기 어렵다는 것입니다.

그 이유는 변경점만 보면 나쁜 코드가 아닌 것처럼 보이기 때문입니다. 하지만 전반적으로 보면 엉망입니다.

After 코드는 발표자가 실제 처음 PR을 올린 코드입니다.

해결 (리팩토링)

다음과 같은 리팩토링을 통해 문제를 해결할 수 있습니다.

  1. 함수의 상세 구현 계층을 통일했습니다. 기존 함수명을 handleQuestionSubmit에서 handleNewExpertQuestionSubmit으로 변경하고, handleMyExpertQuestionSubmit 함수를 추가하여 함수의 계층을 통일했습니다.
    1. handleNewExpertQuestionSubmit에는 사용자가 새로운 담당자에게 보내는 로직만 넣었습니다.
    2. handleMyExpertQuestionSubmit에는 이미 연결된 담당자에게 보내는 로직만 넣었습니다.
function QuestionPage() {
  const myExpert = useFetchMyExpert();

  async function handleNewExpertQuestionSubmit() {
    await sendQuestion(questionValue);
    alert("질문이 등록되었습니다.");
  }

  async function handleMyExpertQuestionSubmit() {
    await sendQuestionToMyExpert(questionValue, expert.id);
    alert(`${myExpert.name}에게 질문했습니다.`);
  }
  1. 팝업 관련 코드를 하나로 모았습니다.

    • 이전 코드는 팝업을 여는 버튼과 팝업 코드가 떨어져 있었는데, 이를 모아서 PopupTriggerButton이라는 컴포넌트를 만들었습니다.
  2. 하나의 함수가 하나의 역할을 하도록 분리했습니다.

    • 이용약관 동의 함수를 openPopupToNotAgreeUsers라는 이름으로 함수를 만들어, 필요할 때 호출하도록 했습니다.
function QuestionPage() {
  const myExpert = useFetchMyExpert();

  async function handleNewExpertQuestionSubmit() {
    await sendQuestion(questionValue);
    alert('질문이 등록되었습니다.');
  }

  async function handleMyExpertQuestionSubmit() {
    await sendQuestionToMyExpert(questionValue, expert.id);
    alert(`${myExpert.name}에게 질문했습니다.`);
  }

  async function openPopupToNotAgreeUsers() {
    // 이용약관 확인
    const agree = await getTargetElement();
    if (!agree) {
      // 이용약관이 필요한 경우, 팝업 표시
      await openAgreePopup();
    }
  }

  return (
    <Main>
      <form>
        <textarea placeholder="질문 내용을 입력해주세요" />
        <button onClick={handleQuestionSubmit}>질문 등록</button>
        {myExpert.connected ? (
          <PopupTriggerButton
            popup={<MyExpertPopup onSubmit={handleMyExpertQuestionSubmit} />}
          >
            질문하기
          </PopupTriggerButton>
        ) : (
          <Button
            onClick={async () => {
              await openPopupToNotAgreeUsers();
              await handleNewExpertQuestionSubmit();
            }}
          >
            질문하기
          </Button>
        )}
      </form>
    </Main>
  );
}

실무에서의 Clean Code란?

코드가 처음 버전보다 길어졌습니다.

Clean Code != 짧은 코드

실무에서의 Clean Code는 짧은 코드가 아니라, 원하는 로직을 빠르게 찾을 수 있는 코드입니다.

Clean Code == 원하는 로직을 빠르게 찾을 수 있는 코드

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

원하는 로직을 빠르게 찾을 수 있도록 하려면, 하나의 목적을 가진 코드가 여기저기 있는 경우 응집도를 높여 모을 필요가 있고, 함수가 여러 역할을 하는 경우 단일 책임 원칙으로 함수를 분리해야 합니다.

그리고 함수의 상세 구현 계층이 다를 때는 추상화 단계를 조절하여, 중요한 개념을 필요한 만큼만 밖으로 노출해야 합니다.

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

  • 하나의 목적을 가진 코드가 흩어져 있다 → 응집도(Cohesion)
  • 함수가 여러 역할을 한다 → 단일 책임(Single Responsibility)
  • 함수의 상세 구현 계층이 제각각이다 → 추상화(Abstraction)

실제 코드를 보면서 하나씩 설명합니다.

응집도(Cohesion)

같은 목적의 코드는 모아 줍니다.

실제 코드입니다. 팝업을 조작하는 코드가 3곳에 나뉘어 있습니다.

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

  async function handleClick() {
    setPopupOpened(true);
  }

  function handlePopupSubmit() {
    await sendQuestionToMyExpert(questionValue, expert.id);
    alert(`${myExpert.name}에게 질문했습니다.`);
  }

  return (
    <>
      <button onClick={handleClick}>질문하기</button>
      <Popup title="보험 질문하기" open={popupOpened}>
        <div>담당자가 설명합니다</div>
        <button onClick={handlePopupSubmit}>확인</button>
      </Popup>
    </>
  )
}

코드를 한눈에 파악할 수도 없고, 버그 발생 위험성도 높은 코드입니다.

Refactor V1

Custom Hook을 사용하여 한 곳에 모았습니다.

function QuestionPage() {
  const [openPopup] = useMyExpertPopup(myExpert.id);

  async function handleClick() {
    openPopup();
  }

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

이제 openPopup 함수를 호출하면 팝업을 열 수 있습니다.

하지만 차분히 보면, 이 코드는 읽기 어려운 코드가 되었습니다.

어떤 팝업이 표시되는지, 팝업의 버튼을 눌렀을 때 어떤 액션을 하는지가 이 페이지에서 가장 중요한 포인트인데, 이것이 모두 hooks에 숨겨져 알 수 없게 되었습니다.

이것은 Custom Hooks의 대표적인 안티패턴입니다. 코드를 보고 지저분하다고 생각하면, 일단 hooks로 전부 모으는 것입니다.

그렇다면 무엇을 모아야 할까요?

지금 당장 몰라도 되는 상세 구현입니다. 이를 숨겨두면, 짧은 코드만 봐도 빠르게 코드의 목적을 파악할 수 있습니다.

반대로 모으면 보기 어려워지는 것은, 코드를 파악하기 위해 필수적인 정보입니다. 이를 분리해두면, 여러 모듈을 왔다 갔다 하면서 코드의 흐름을 확인하는 상태가 됩니다.

Clean Code != 짧은 코드

코드를 모아서 짧은 코드를 만드는 것이 Clean Code가 되는 것은 아닙니다.

Clean Code는 찾고 싶은 로직을 빠르게 찾을 수 있는 코드입니다.

어떻게 하면 읽기 쉽게 응집할 수 있을까요?

코드 응집 Tip: 코어 데이터와 상세 구현을 분리하라

먼저, 남겨야 할 코어 데이터와 숨겨도 되는 상세 구현을 분리해봅시다.

이 코드에서 코어 데이터는 팝업 버튼을 클릭했을 때 실행할 액션과 팝업의 제목, 내용입니다.

function QuestionPage() {
  /*
   * 상세 구현: 팝업 버튼 클릭 시 액션
   */
  const [popupOpened, setPopupOpened] = useState(false);

  async function handleClick() {
    setPopupOpened(true);
  }

  function handlePopupSubmit() {
    /*
     * 코어 데이터: 팝업 버튼 클릭 시 액션
     */
    await sendQuestionToMyExpert(questionValue, expert.id);
    alert(`${myExpert.name}에게 질문했습니다.`);
  }

  return (
    <>
      <button onClick={handleClick}>질문하기</button>
      /*
       * 코어 데이터: 제목, 내용
       * 상세 구현: 컴포넌트의 Markup과 버튼 클릭 시 함수 호출
       */
      <Popup title="보험 질문하기" open={popupOpened}>
        <div>담당자가 설명합니다</div>
        <button onClick={handlePopupSubmit}>확인</button>
      </Popup>
    </>
  )
}

상세 구현은 팝업을 열고 닫는 State와 컴포넌트의 세부 Markup, 그리고 팝업의 버튼을 클릭했을 때 특정 함수를 호출하도록 하는 바인딩입니다.

Refactor V2

여기서 코어 데이터를 남기고 상세 구현을 숨기면, 파악하기 쉬운 코드가 됩니다.

openPopup이라는 Custom Hook에 모든 코드를 숨기는 것이 아니라, 상세 구현만 숨기고 코어 데이터인 팝업의 제목, 내용, 액션은 밖에 남깁니다.

function QuestionPage() {
  const [openPopup] = usePopup();

  async function handleClick() {
    const confirmed = await openPopup({
      /*
       * 팝업의 제목, 내용
       */
      title: '보험 질문하기',
      contents: <div>담당자가 설명합니다</div>,
    });

    if (confirmed) {
      /*
       * 팝업의 액션
       */
      await submitQuestion();
    }
  }

  async function submitQuestion(myExpert) {
    await sendQuestionToMyExpert(questionValue, expert.id);
    alert(`${myExpert.name}에게 질문했습니다.`);
  }

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

그러면 상세 구현을 읽지 않아도, 어떤 팝업인지 파악할 수 있습니다.

선언형 프로그래밍

이런 개발 스타일, 즉

팝업, 너에게 선언한다!

제목은 "보험 질문하기"

내용은 "전문가가 설명합니다."

"그리고 확인 버튼을 클릭하면, 질문을 전송해!"

라고 선언하면, 팝업이 사전에 구현된 상세 구현으로 그 내용을 표시하는 스타일, 이것을 선언형 프로그래밍이라고 합니다.

선언형 프로그래밍의 특징은, 함수가 무엇을 하는지 빠르게 이해할 수 있다는 것입니다.

상세 구현을 숨겨서 신경 쓸 필요가 없고, 이 “무엇(What)“을 바꿔서 쉽게 재사용할 수 있다는 점입니다.

<Popup onSubmit={질문전송} onSuccess={홈으로이동} />

선언형으로 모으지 않고, 하나하나 상세 구현을 작성하는 방법은 명령형 프로그래밍이라고 합니다.

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

선언형 프로그래밍도 내부를 보면 이처럼 명령형으로 작성되어 있습니다. 상세 구현이 모두 공개되어 있으므로 커스터마이징은 쉽지만, 읽는 데 시간이 걸리고 재사용하기 어렵습니다.

Q: 선언형 프로그래밍이 절대적으로 좋은가요? A: 아닙니다. 두 가지 방법을 활용하여 작성하면 됩니다.

선언형 프로그래밍이 명령형 프로그래밍보다 트렌드니까 더 좋은 것인지 생각하는 분도 있을 수 있지만, 각각 장점이 있습니다.

React의 JSX 문법에서는 HTML에서도 선언형 프로그래밍을 할 수 있는 장점이 있지만, props로 “무엇(What)“을 전달하는 것, 이러한 상세 내용을 전달하는 경우에는 명령형 설계도 필요합니다.

단일 책임

하나의 일을 한다는 것이 알 수 있는 명확한 이름을 가진 함수를 만들어야 합니다.

다음과 같의 이벤트 핸들러 함수를 만들어봅시다. 폼에서 질문 전송 버튼을 클릭했을 때의 함수 이름입니다.

async function 〇〇〇〇() {
  const 이용약관동의 = await 이용약관동의조회();
  if (!이용약관동의) {
    await 이용약관동의팝업();
  }
  await 질문전송(questionValue);
  alert('질문이 등록되었습니다');
}

이용약관에 동의했는지 체크하고, 질문을 전송합니다.

질문을 전송하는 것이 핵심 기능이므로, handle질문전송 정도가 좋지 않을까요?

async function handle질문전송() {
  const 이용약관동의 = await 이용약관동의조회();
  if (!이용약관동의) {
    await 이용약관동의팝업();
  }
  await 질문전송(questionValue);
  alert('질문이 등록되었습니다');
}

아닙니다! 좋지 않습니다! 함수명은 질문전송이지만, 구현 내용에는 이용약관 체크, 질문 전송이 혼재되어 있습니다.

async function handle질문전송() {
  /*
   * 이용약관 체크
   */
  const 이용약관동의 = await 이용약관동의조회();
  if (!이용약관동의) {
    await 이용약관동의팝업();
  }
  /*
   * 질문 전송
   */
  await 질문전송(questionValue);
  alert('질문이 등록되었습니다');
}

이처럼 중요한 포인트가 모두 포함되지 않은 함수명은, 읽는 사람이 예상한 대로 코드가 동작하지 않으므로 코드에 대한 신뢰 저하로 이어집니다.

그 이후부터는 함수명을 신뢰하지 않게 되어, 상세 내용을 모두 확인하게 됩니다.

여기서 기능 추가가 들어오면 어떻게 될까요.

async function handle질문전송() {
  const 이용약관동의 = await 이용약관동의조회();
  if (!이용약관동의) {
    await 이용약관동의팝업();
  }
  await 질문전송(questionValue);
  alert('질문이 등록되었습니다');
  const 보험담당자 = await 보험담당자조회();
  if (보험담당자 !== null) {
    await 보험담당자에게질문전송(questionValue);
    alert(`${보험담당자.name}에게 질문을 전송했습니다.`);
  }
}

함수가 더 비대해지고, handle질문전송이라는 이름에 질문전송 외에도 2가지 역할을 하게 됩니다.

이와 같이 이미 있는 함수에 기능만 추가하는 것을 다들 해본적이 있을 겁니다.

이러한 기능 추가가 반복되면, 미래의 우리가 다른 사람에게 하는 말이 있습니다.

그 코드는 안 건드리는 게 좋을 것 같아요.

일단 제가 수정할게요.

하나의 역할을 하는 명확한 이름의 함수가 되도록 리팩토링해보았습니다.

async function handle보험담당자에게질문전송() {
  await 보험담당자에게질문전송(questionValue);
  alert(`${보험담당자.name}에게 질문을 전송했습니다.`);
}

async function handle새로운질문전송() {
  await 질문전송(questionValue);
  alert('질문이 등록되었습니다');
}

async function handle이용약관동의확인() {
  const 이용약관동의 = await 이용약관동의조회();
  if (!이용약관동의) {
    await 이용약관동의팝업();
  }
}

이처럼 분리하여, 필요할 때 각각 읽고 사용하면 됩니다.

단일 역할의 기능성 컴포넌트

이렇게 함수를 분리하는 것처럼, React 컴포넌트도 기능별로 분리할 수도 있습니다.

<button
  onClick={async () => {
    log('전송 버튼 클릭');
    await openConfirm();
  }}
/>

버튼을 클릭하면 서버에 로그를 남기는 코드가 있습니다.

여기서 아쉬운 점은 버튼 클릭 함수에 로그를 남기는 함수와 API 호출이 혼재되어 있다는 것입니다.

<LogClick message="전송 버튼 클릭">
  <button onClick={openConfirm} />
</LogClick>

LogClick이라는 컴포넌트를 만들어, 버튼을 클릭하면 자동으로 클릭 로그가 전송되도록 리팩토링하면 좋습니다.

이렇게 하면, 버튼 클릭 함수에서는 API 호출만 신경 쓸 수 있습니다.

그리고 요소가 겹치는지 판단하는 IntersectionObserver 코드가 있습니다.

const targetRef = useRef(null);

useEffect(() => {
  const observer = new IntersectionObserver(([{ isIntersecting }]) => {
    if (isIntersecting) {
      fetchCats(nextPage);
    }
  });
  return () => {
    observer.unobserve(targetRef.current);
  };
});

return <div ref={targetRef}>더 보기</div>;

Observer 코드의 상세 구현과 API를 호출하는 것이 혼재되어 있는 것이 아쉽습니다.

이것은 IntersectionArea라는 상위 컴포넌트를 만들어, Intersection의 상세 코드와 API 호출 부분을 분리할 수 있습니다.

<IntersectionArea onImpression={() => fetchCats(nextPage)}>
  <div ref={targetRef}>더 보기</div>
</IntersectionArea>

추상화(Abstraction)

추상화를 통해 로직에서 핵심 개념을 뽑아낼 수 있습니다.

다음 팝업 컴포넌트의 코드는 처음부터 세밀하게 구현한 것입니다.

/* 팝업 코드를 처음부터 구현 */
<div style={팝업스타일}>
  <button
    onClick={async () => {
      const res = await 회원가입();
      if (res.success) {
        프로필이동();
      }
    }}
  >
    전송
  </button>
</div>

다음은 이 팝업 코드를 전송 액션과 성공 액션이라는 중요한 개념만 남기고, 나머지는 추상화한 것입니다.

/* 중요 개념만 남기고 추상화 */
<Popup onSubmit={회원가입} onSuccess={프로필이동} />

함수의 추상화 예도 봅시다.

다음은 담당자 정보를 조회하여, 조회한 정보에 따라 다른 라벨을 표시하는 코드입니다.

/* 담당자 라벨을 조회하기 위한 코드의 상세 구현 */
const planner = await fetchPlanner(plannerId);
const label = planner.new ? '새로운 담당자' : '연결된 담당자';

다음은 이 상세 구현을 getPlannerLabel이라는 함수명 안에 모두 추상화한 코드입니다.

/* 중요 개념을 함수명에 넣어 추상화 */
const label = await getPlannerLabel(plannerId);

이와 같이 컴포넌트, 함수 등과 같은 코드는 구체적인 코드를 조금 추상적으로, 또는 더 추상적으로 리팩토링할 수 있습니다.

한번 더 살펴봅시다.

다음과 같이 버튼을 클릭할 때, Confirm을 표시하고, 여기서 Confirm 버튼을 누르면 특정 메시지를 표시하는 구체적인 코드가 있습니다.

/* Level 0 */
<Button onClick={showConfirm}>전송</Button>;
{
  isShowConfirm && (
    <Confirm
      onClick={() => {
        showMessage('성공');
      }}
    />
  );
}

버튼이 눌렸을 때, Confirm을 표시하는 기능을 ConfirmButton이라는 컴포넌트로 추상화해보았습니다.

onConfirm을 사용하여, 눌렸을 때 발생하는 액션을 전달할 수 있습니다.

/* Level 1 */
<ConfirmButton
  onConfirm={() => {
    showMessage('성공');
  }}
>
  전송
</ConfirmButton>

이 코드를 사용해보니, message라는 props만 전달하여, Confirm에 표시하고 싶은 메시지를 표시하도록 더 간단하게 추상화할 수도 있습니다.

/* Level 2 */
<ConfirmButton message="성공">전송</ConfirmButton>

여기서 한 발 더 나아가, 모든 기능을 ConfirmButton이라는 이름에 추상화할 수도 있습니다.

/* Level 3 */
<ConfirmButton />

정답은 없습니다. 상황에 따라 필요한 만큼 추상화하여 사용하면 됩니다.

실제 리뷰 예시입니다.

  • 예시 1

【리뷰어】 이 부분을 전부 추상화할 수는 없나요? await moreAccurateLocation.request() 이런 식으로요. 부모가 모달을 여는 부분을 알 필요가 없다고 생각했습니다.

【코드 작성자】 좋네요. moreAccurateLocation.check()이라는 이름은 어떨까요?

  • 예시 2

【리뷰어】 UltraCallProgress와 UltraCallComparison이 비슷하게 보이는데, 공통 부분을 추상화할 수 없을까요?

【코드 작성자】 이 부분은 아직 코드 중복이 심하지 않아서, 향후 유연성을 생각하면 다른 부분이 생길 것 같아서 일부러 추상화하지 않았습니다. (초기 단계에서 추상화하여 유지보수가 힘들었던 적이 꽤 있었습니다.) 좀 더 생각해보겠습니다.

추상화 레벨이 혼재되면 코드 파악이 어렵습니다. 추상화 레벨이 혼재되면 전반적으로 코드가 어느 수준에서 구체적으로 작성되어 있는지 파악하기 어렵습니다.

다음과 같이 추상화 레벨이 혼재되어 있으면, 구체적으로 작성된 코드를 보고 그 다음에 나오는 높은 수준으로 추상화된 코드도 구체적으로 작성되어 있다고 착각하게 됩니다.

<Title>평가해주세요.</Title>
<div>
  {STARS.map(() => <Star />)}
</div>
<Reviews />
{rating !== 0 && (
  <>
    <Agreement />
    <Button rating={rating} />
  </>
)}

그래서 높은 수준으로 추상화된 코드가 작은 코드라고 생각하고 내부를 보면, 상당히 복잡한 코드가 나올 때가 있습니다.

이렇게 작성하면, 코드를 읽을 때 사고가 정리되지 않습니다. 코드가 어느 수준에서 구체적으로 작성되어 있는지 파악할 수 없기 때문입니다.

<Title>평가해주세요.</Title>
<Stars />
<Reviews />
<AgreementButton show={rating !== 0}/>

이와 같이 비슷한 추상화 레벨로 맞춰 작성하면, 코드가 더 파악하기 쉬워집니다.

여기서는 높은 수준의 추상화로 정리했지만, 상황에 따라 낮은 수준의 추상화로 정리해도 괜찮습니다.

액션 아이템

이론은 공부했으니, 내일부터 여러분의 프론트엔드 코드에도 Clean Code를 적용해보세요.

그를 위한 액션 아이템을 소개합니다.

두려워하지 말고 기존 코드를 수정하자

코드를 깨는 것이 두려우면, 클린한 실무 코드를 만들 수 없습니다.

Pull Request의 File Changes가 많아진다는 핑계로, 우리는 기존 코드를 깨지 않고 코드를 추가합니다.

두려워하지 말고, 기존 코드와 아키텍처를 깨고 코드를 작성해보세요.

Pull Request의 File Changes를 줄이고 싶다면, Mother Branch를 만들어 리팩토링을 추가하는 방법도 있습니다.

전체 그림을 보는 연습

그때는 맞았던 것이, 지금은 다를 수 있습니다.

기존 코드가 깔끔했지만, 내가 추가한 코드로 엉망이 되는 경우가 있습니다.

내가 추가한 기능 자체는 깔끔할 수 있지만, 전체 그림에서 보면 깔끔하지 않은 코드일 수 있습니다.

팀과 공감대를 형성하자

코드에는 정답이 없습니다.

그래서 코드 리뷰 시, Clean Code에 관한 코멘트를 남겨도 될지 망설이는 때가 있습니다.

당장은 작은 이슈라 코멘트를 남기는 것을 주저하지만, 이렇게 작은 일관성을 깨는 코드가 쌓이면 유지보수하기 어려운 코드가 됩니다.

공감대는 자동으로 만들어지지 않습니다. 명확하게 이야기하는 시간이 필요합니다.

함께 만든 코드에서 고치고 싶은 포인트를 서로 이야기하고, 문제라고 생각하는 포인트를 공유하여 집단 지성을 모으세요.

그리고 어떻게 개선할지 생각하면 됩니다. 개선은 바로 답을 낼 필요가 없습니다. 문제를 공유한 후, 시간을 두고 개선하면 됩니다.

문서로 작성하자

Clean Code는 모호한 개념입니다. 글로 적으면 명확해집니다.

이 코드가 장래에 어떤 포인트에서 위험해지는지, 어떻게 개선할 수 있는지 자신만의 원칙을 적어보세요.

완료

이 포스트에서 정리한 React Clean Code의 핵심 개념을 요약하면 다음과 같습니다.

  • 응집도: 같은 목적의 코드를 모으되, 코어 데이터와 상세 구현을 분리하라
  • 단일 책임: 하나의 역할을 하는 명확한 이름의 함수를 만들어라
  • 추상화: 핵심 개념을 뽑아내되, 추상화 레벨을 통일하라

이 글의 내용은 Toss의 SLASH 21 컨퍼런스에서 발표된 내용을 기반으로 합니다.

제 블로그가 도움이 되셨나요? 하단의 댓글을 달아주시면 저에게 큰 힘이 됩니다!

앱 홍보

책 홍보

블로그를 운영하면서 좋은 기회가 생겨 책을 출판하게 되었습니다.

아래 링크를 통해 제가 쓴 책을 구매하실 수 있습니다.
많은 분들에게 도움이 되면 좋겠네요.



SHARE
Twitter Facebook RSS