[SLASH21] 실무에서 바로 쓰는 Frontend Clean Code - 토스 개발자 컨퍼런스 2021

마리 Mari·2021년 5월 2일
0
post-thumbnail

https://toss.im/slash-21/sessions/3-3


2021 토스 개발자 컨퍼런스에서 클린코드에 관한 세션이 너무 유익하고 배워갈 점이 많아 정리해보려 한다. 요즘 부쩍 '코드로 구현하는 방법' 보다는 '디자인 패턴', '협업 가능한 코드' 와 같이 '좋은 코드를 작성하는 방법'에 관심이 많아진터라 그런지 무척 재미있었다.

위 세션에서는 클린코드란 무엇이며, 클린코드가 왜 실무에서 중요하고, 클린코드로 리팩토링하는 방법에 대해 이야기한다.



1. 실무에서 클린 코드의 의의


지뢰 코드의 존재

그 코드는 안 건드리시는게 좋은 거에요.
일단 제가 만질게요. ^^;;

협업을 하다보면 공공연한 지뢰코드가 있는 경우가 많다. 이러한 코드들은 흐름파악이 어렵고, 맥락 표현이 안되어 있어, 동료에게 물어봐야 알 수 있는 코드이다. 이는 주로 기존 코드에 기능을 추가하는 과정에서 생겨난다.

지뢰코드의 존재는 유지보수를 할 때 개발시간을 길어지게 만든다. 뿐만 아니라 기능추가가 불가능하게 하거나, 성능을 저하시킬 수도 있다. 실무에서 클린코드는 곧 유지보수 시간의 단축이다. 시간은 자원이고 자원은 돈이기에, 시간의 단축은 비용 감소인 것이다.

처음 코드를 설계하고 새로운 파일을 작성할 때는 대체로 '클린'하다. 하지만 기능을 추가하는 과정에서 방심하면 금새 코드가 들쑥날쑥 해진다. 실무의 90%는 기존 코드에 기능을 추가하는 일이기에, 코드를 '클린'하게 유지하는 것에 신경을 쓸 수밖에 없다.


2. 안일한 코드 추가의 함정


나쁜 코드는 어떻게 만들어질까

유지보수 시점에서 코드가 어떻게 나빠지는지게 되는지 실제 사례를 통해 보여준다.

발표하신 개발자분께서 이를 함정이라고 표현하였다. 겉으로 보기에는 성공적으로 기능이 추가된 것처럼 보이지만, 클린코드의 관점에서는 나쁜 코드가 됐기 때문이다.

예시 사례의 유지보수 상황은 다음과 같다.

요청사항

  • 보험 관련 질문 입력 페이지에서
  • 질문하기 버튼을 눌렀을 때
  • 사용자에게 배정된 설계사가 있지 확인하고
  • 있는 경우, 설계사 사진이 들어간 팝업을 띄우기

기존 코드

  • 질문하기 버튼을 누르면
  • 사용자의 약관동의 여부를 확인
  • 약관동의가 필요하면 약관동의 팝업열기
  • 약관동의 후 질문을 전송, 성공했다는 Alert 띄우기
function QuestionPage() {
  async function handleQuestionSubmit() {
    const 약관동의 = await 약관동의_받아오기();
    if(!약관동의) {
      await 약관동의_팝업열기();
    }
    await 질문전송(questionValue);
    alert ("질문이 등록되었어요.");
  }
  return (
    <main>
      <form>
        <textarea placeholder="어떤 내용이 궁금한가요?"/>
        <Button onClick={handleQuestionSubmit}>질문하기</Button>
      </form>
      ...

요청사항에 맞게 변경하기 위해 코드를 다음과 같이 변경하였다.

  • 기존 handleQuestionSubmit() 함수에서 연결 중인 전문가가 있는지 확인하는 코드를 추가
  • 연결 중인 전문가가 있는 경우, 팝업을 띄우는 로직을 추가
  • 팝업 컴포넌트 추가(평소에는 hide, 필요한 경우 show)
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(`${연결전문가.name}에게 질문이 등록되었어요.`);
  }
  return (
    <main>
      <form>
        <textarea placeholder="어떤 내용이 궁금한가요?"/>
        <Button onClick={handleQuestionSubmit}>질문하기</Button>
      </form>
      {popupOpened && (
        <연결전문가팝업 onSubmit={handleMyExpertQuestionSubmit} />
      )}
    </main>
  );


추가된 코드들은 타당해보이지만, 클린코드의 관점에서 나쁜 코드가 되었다.
그 이유는 다음과 같다.

1. 하나의 목적인 코드가 흩뿌려져 있음

  • 연결 중인 전문가 팝업 관련 코드들이 떨어져있어, 추후에 기능 추가시 스크롤을 이동하며 코드를 보아야 함

2. 하나의 함수가 여러가지 일을 하고 있음

  • handleQuestionSubmit()함수가 연결전문가 확인, 약관동의 확인, 질문 전송까지 3가지 일을 하고 있음

3. 함수의 세부구현 단계가 제각각

  • handleQuestionSubmit()함수와 handleMyExpertQuestionSubmit()함수 둘다 이벤트 핸들링 함수이며 그 이름이 유사하나, handleQustionSubmit()이 질문 전송외에 다른 일들 까지 하고 있음

풀 리퀘스트에서 변경사항만 보면 어지러운 코드라는 것을 파악하기 어렵다.
하지만 전체그림으로 보았을 때 엉망인 코드인 것을 알 수 있다.


클린하게 리팩토링 해보기

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

  • 함수명 위계 맞추기. handle {New, My} ExpertQuestionSubmit
  • handleNewExpertQuestionSubmit() : 새로운 전문가에게 질문하는 로직만
  • handleMyExpertQuestionSubmit() : 연결 중인 전문가에게 질문하는 로직만
function QuestionPage() {
  const 연결전문가 = useFetch(연결전문가_받아오기);
  
  async function handleNewExpertQuestionSubmit(){
    await 질문전송(questionValue);
    alert("질문이 등록되었어요.");
  }
  
  async function handleMyExpertQuestionSubmit(){
    await 연결전문가_질문전송(questionValue, 연결전문가.id);
    alert(`${연결전문가.name}에게 질문이 등록되었어요.`);
  }

2. 팝업 관련 코드를 하나로 뭉치기

  • 기존의 팝업 여는 버튼과 팝업 코드가 떨어져 있는 것을 합친 컴포넌트 <PopupTriggerButton> 추가
  • 띄울 팝업을 props로 전달
return (
  <main>
    <form>
      <textarea placeholder="어떤 내용이 궁금한가요?" />
      {연결전문가.connected ? (
        <PopupTriggerButton
          popup={(
            <연결전문가팝업 onButtonSubmit = {handleMyExpertQuestionSubmit}/> 
          )} >질문하기</PopupTriggerButton>
      ) : ( ...

3. 함수마다 단일한 일을 하도록 분리

  • handleQuestionSubmit()안에 있던 약관동의 관련 코드를 쪼개어 함수로 만든 후, 필요한 시점에만 부르도록 변경
        <Button onClick={async () => {
            await openPopupToNotAgreedUsers();
            await handleNewExpertQuestionSubmit();
          }} 
        >
        질문하기
        </Button>
      )}
      </form>
    </main>
  );
}
async function openPupupToNotAgreedUsers() {
  const 약관동의 = await 약관동의_받아오기();
  if(!약관동의) {
    await 약관동의_팝업열기();
  }
}


3. 클린코드란?


!= 짧은 코드

클린코드를 짧은 코드로 착각하기 쉽다. 하지만 위의 사례를 통해 코드가 더 길어졌음에도 코드의 내용을 파악하기가 더 쉽게 변한 것을 볼 수 있다.

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

원하는 로직을 빠르게 찾을 수 있으려면? 코드에 다음의 개념들이 잘 적용되어야 한다.

1. 응집도

= 하나의 목적을 가진 코드들이 뭉쳐져 있어야 한다.

같은 목적의 코드가 모여있으면 코드가 간결해지며, 관련 기능을 수정할 때 편리하다.
그러나 단순히 뭉치기만 하는 것으로 클린코드가 되지 않으며, 무작정 코드를 뭉치는 것은 오히려 코드의 가독성을 떨어트릴 수 있다.

같은 목적의 코드를 하나의 모듈로 분리했다고 하자. 해당 모듈이 사용되는 시점에서 이것이 어떤 내용이며, 어떤 액션을 하는 지 파악할 수 없다면, 분리해놓은 모듈의 코드와 사용되는 시점의 코드를 넘나들면서 흐름을 따라가야 하는 상황이 발생하게 된다.

이러한 문제를 방지하기 위해 코드 파악에 필수적인 핵심 정보와 당장은 몰라도 되는 세부 구현을 분리하여 응집화를 적용해야한다. 그 후 핵심 정보만 전달하면 모듈이 세부 구현을 적용하여 내용을 뿌려주도록 모듈을 구현하는 것이다.

이와 같은 방식을 선언적 프로그래밍이라고 한다. 함수 기능 파악이 쉬우며, 재사용성이 높다는 장점이 있다. 반대로 세부 구현을 모두 기술하는 방식을 명령형 프로그래밍이라고 한다. 선언적 프로그래밍에 비해 코드파악이 어려우며 재사용성이 낮지만, 세부 구현이 드러나있어 커스텀하기 쉽다는 장점이 있다.

두 방법 중 어느 하나가 좋다고 말하기 어려우며, 상황에 따라 유동적으로 선택하여 사용하는 것이 바람직하다.

2. 단일책임

함수가 단 하나의 일만 수행해야 한다.

함수가 하나의 일을 하며, 함수의 이름이 이를 뚜렷하게 표현해주어야 한다. 함수의 주요 기능이 담겨져있지 않는 함수 명은 코드 파악을 어렵게 만든다. 읽는 이가 예상한대로 코드가 작동하지 않으며, 함수의 세부구현을 모두 샅샅이 살펴보아야 하게 만든다. 또한, 이미 있는 함수에 기능을 추가만 하는 것도 위와 같은 문제를 야기할 수 있다.

단일책임 리팩토링 Tip

  1. 한 가지 일만 하는, 명확한 이름의 함수 만들기
  2. 한 가지 일만 하는, 기능성 컴포넌트 만들기
  3. (소소한 팁) 조건이 많아지면 한글 이름도 유용하다

3. 추상화

추상화 단계 조절하여 필요한 만큼만 핵심 개념을 노출한다

예시 1. 팝업 컴포넌트 코드 추상화

아래는 팝업 컴포넌트를 디테일하게 구현한 코드이다.

<div style={팝업스타일}>
  <button onclick={async () => {
      const res = await 회원가입();
      if(res.success) {
        프로필로 이동();
      }
    }}>전송</button>
</div>

여기에서 제출액션, 성공액션이라는 중요 개념만 남기고 추상화를 진행하였다.

<Popup
  onSubmit={회원가입}
  onSuccess={프로필로이동}
/>

예시 2. 전문가 정보 라벨 코드 추상화

아래는 전문가 정보를 가져온 후 응답 값에 따라 다른 라벨을 보여주는 코드이다.

const planner = await fetchPlanner(plannerId)
const label = planner.new ? '새로운 상담사' : '연결중인 상담사'

label의 세부 구현을 getPlannerLabel이라는 함수 안에 모두 추상화시켰다.

const label = await getPlannerLabel(plannerId)

상황에 따라 원하는 단계까지 추상화를 진행하면 된다. 항상 높은 추상화 단계가 옳은 것은 아니며, 상황과 목적에 따라 적절한 추상화 단계를 선택하는 것이 좋다.

다만 추상화를 할 때 한 코드 내에서 추상화 수준이 섞여있지 않도록 주의해야 한다. 전체적인 코드가 어느 수준으로 구체적으로 기술된지 파악할 수 없어, 코드 파악이 어려워진다. 구체적으로 기술된 컴포넌트를 보고 높은 추상화의 컴포넌트도 구체적으로 기술되었다고 생각하는 오류를 범할 수도 있다.

반대로 추상화 단계가 비슷하게 정리되어 있으면 코드를 물 흐르듯이 이해할 수 있다.



4. 클린코드를 바로 적용해보자!


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

기존 코드의 구조 뜯기를 두려워하면, 클린한 코드를 유지할 수 없다. 두려워하지 말고 마음껏 코드를 씹고 뜯고 맛보고 즐기자는 마음가짐이 필요하다.

혹여 풀 리퀘스트에 file changed가 많아지는 것이 신경쓰인다면, branch를 이용하여 리팩토링한 pr을 추가로 만드는 방법을 사용해보자.

2. 큰 그림 보는 연습하기

섣부른 기능 추가 코드가 기존의 클린한 코드를 망칠 수 있다.
추가된 코드 자체는 클린해도, 전체적인 그림에서는 어지러울 수 있다는 것을 유념하자.

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

팀원 간의 코드 리뷰시, 클린코드와 관련된 피드백을 주저하지 말자.
당장은 사소한 이슈일지라도, 이것이 누적되면서 일관성없는 코드가 만들어지게 된다.

코드에서 고치고 싶은 포인트를 팀원들에게 명시적으로 이야기해야, 팀원간의 공감대를 형성할 수 있다. 문제라고 생각되는 지점을 공유하면 함께 집단지성을 모을 수 있다. 문제점이 일단 공유되면 바로 해결되지 않더라도, 팀원들이 시간을 갖고 개선방안을 함께 고민해나갈 것이다.

4. 문서로 적어보기

클린코드는 모호한 개념, 글로 적어야 명확해진다.
이 코드가 어떤 점에서 향후 위험할 수 있는지, 어떻게 개선할 수 있는지 나만의 원칙을 적어보자.




세션을 들으면서 지난 날에 내가 작성했던 코드들이 떠올랐다.

시간이 없다는 이유로 무작정 기능을 추가하고 구현은 잘 되니 넘기고, 다시 꺼내보지 않았던 코드들... 오랜만에 다시 그 코드들을 꺼내보고 수정해봐야겠다는 다짐이 들었다.

리팩토링을 해탸겠다는 결심이 서도 어디서부터 봐야할지 엄두가 안나 하지 못했다. 내가 쓴 걸 내가 왜 두려워하는지 모르겠지만, 두려운 마음이 앞섰다. 그래서 수정하는 게 아니라 코드를 처음부터 다시 작성하면서 리팩토링을 하곤 했다. 이전에 짰던 코드는 참고만 하면서 코드를 새로 작성하는 짓을 했다. 깨끗한 환경에서 다시 시작하니 의욕도 넘치고 명쾌한 기분이었지만, 어차피 결국 다시 코드는 더러워졌다. 더불어 시간도 배로 들었다. 그렇게 악순환을 반복했다.

발표자분께서 마지막에 이야기하신 담대하게 기존 코드 수정하기를 마음 속에 새기고 묵혀둔 레포지터리를 들춰보자. 이제 클린코드로 리팩토링하는 방법도 배웠으니 더 이상 모른척 하지 말자! 화이팅...!

profile
우리 블로그 정상영업합니다.

0개의 댓글