난잡해진 프로젝트의 코드 개선 필요성

 2명이서 프로젝트를 진행하면서 급한 기능 구현에 많은 시간을 할애했습니다. 기능 구현을 진행하면서 전체 코드 중 약 10에서 30 정도만 건드렸고, 나머지는 몇 개월 전의 상태 그대로 유지되고 있습니다.

 일부 코드는 ‘hot’ 또는 ‘warm’에 속하며 코드 컨벤션, 함수명 또는 변수명 개선 사항이 적용되었습니다. 그러나 나머지 코드는 레거시 코드로 남아있어 수정되지 않았습니다. 이를 전체적으로 수정하기 위해 eslint와 husky를 도입했으나, 이것이 감지하지 못하는 레거시 코드도 있습니다. 예를 들어, 이름만으로 파악하기 어려운 함수 또는 잘못된 이름의 함수가 있을 수 있습니다.

 기능 구현 일정에 쫓기더라도 ‘cool’한 코드를 ‘cool’한 상태로 유지하는 것은 언젠간 발목을 붙잡을 것이기 때문에, 시간을 내어 개선하기로 결정했습니다..!

 가독성을 향상시키기 위해 여러 가지 방법을 고려하였고, 그 중에서도 카카오의 서종만 님의 섬세한 ISFP의 코드 가독성 개선 경험 영상을 참고하였습니다. 본 포스트에선 이 영상에서 소개된 주제들을 실제 프로젝트에 적용한 대표적인 사례를 보여드리겠습니다.

1. 정확한 단어 고르기

📒 다른 뜻을 가진 단어와 구분하기

 영상에서는 ‘load’의 ‘가져와서 싣다’와 ‘fetch’의 ‘가져오다’의 뉘앙스 차이를 파악하고 React-Query 라이브러리 용법을 익히거나 코드를 읽을 때 한결 수월해진 경험을 공유하고 있습니다. 저도 단어의 뉘앙스 차이를 정확하게 파악하여 라이브러리를 이해하는데 도움이 된 경험이 있어서 공유하고자 합니다.

 React-Query v4에는 paused status가 새롭게 추가되었습니다. 엄밀히 말하자면 status에선 loading, error, success 상태를 관리하고 새롭게 추가된 fetchStatus에서 fetching, paused, idle상태를 관리합니다.

 처음 공식 문서를 읽을 때는 idlepaused가 둘다 멈춰 있는 상태로 해석하고 차이를 정확히 파악하지 못했습니다. 사실 차이가 있다는 것 조차 인지하지 못하고 넘어갔습니다. 따라서 왜 paused가 새로 생긴지 이유를 알지 못했습니다.

 하지만, v4에서 추가된 Network Mode를 읽을 때는 idlepaused의 정확한 차이를 알아야 했습니다. 사실 대강 읽고 넘어가면 놓칠 수 있지만, 두 단어의 차이는 fetchget보다 명확합니다.

 React-Query에서 어느 순간에 query가 아무 것도 하지 않는다면, idle 상태입니다. 즉, 시작조차 하지 않고 대기중이라면 idle이라고 말할 수 있습니다.

 반면, pausedquery가 결과를 가져오기 원하는 상황이지만, 무언가에 의해 멈춰져서 실행하지 못하는 상태입니다. 대표적으로 네트워크가 끊겨서 paused상태가 됩니다.

 그렇다면, paused상태가 v4에서 추가된 이유를 유추할 수 있습니다. v3에선 네트워크 끊김 현상을 대비하지 못했습니다. 만약 네트워크가 끊겨도 항상 첫번째 요청을 보냅니다. 당연히 에러가 반환됩니다. 하지만, v4에선 기본적으로 네트워크가 끊긴다면 query가 paused 상태로 전환됩니다. 만약 다시 네트워크가 접속된다면 paused상태였던 query를 중단된 위치에서 다시 시작합니다.

저의 경우 사내용 서버에 배포된 프로젝트여서 가끔 vpn이 끊기면 동작을 수행할 수 없는 경우가 발생합니다. 다시 vpn에 접속할 때 멈췄던 동작을 알아서 다시 수행하려면 v4의 새로운 기능인 Network Mode가 도움될 것 같습니다.

 물론 v4에서 Network Mode가 생긴 이유는 이것에 한정되진 않습니다. 다만, pausedidle의 차이를 파악함으로써 공식 문서를 읽을 때 paused 상태가 생긴 이유를 정확히 이해할 수 있었습니다.

📖 보다 구체적인 단어로 바꾸기

 다음은 일반적이고 넓은 의미로 사용 가능한 언어들을 보다 구체적인 단어로 바꾼 사례입니다. 예를 들어, 넓은 의미로 사용 가능한 get(가져오다)함수를 extract(추출하다), parse(분해하다), aggregate(합치다) 따위로 바꿀 수 있습니다. 이것의 장점은 개발자가 어느정도 함수의 동작을 예측할 수 있게 도와줍니다. 예를 들어 extract 함수의 output은 input의 일부분일 것으로 예측가능합니다.

 이 방법을 사용하여 실제로 checkDatePassLimit라는 함수명을 수정했습니다. 직역하면 Date가 한계를 통과하는지 확인하라… 인데 한번에 동작을 예측하기 어려운 함수입니다. 함수를 자세히 확인해보겠습니다.

const MILLISECONDS_IN_A_DAY = 86400000;

export const checkDatePassLimit = (
  startDateInMilliSec: number, // 시작 날짜 (Date to Milli Seconds)
  endDateInMilliSec: number, // 종료 날짜 (Date to Milli Seconds)
  startTaskDateTimeInMilliSec: number // 작업 시작 시각 (DateTime to Milli Seconds)
) => {
  if (
    startTaskDateTimeInMilliSec >= startDateInMilliSec &&
    startTaskDateTimeInMilliSec < endDateInMilliSec + MILLISECONDS_IN_A_DAY
  )
    return true;

  return false;
};

 작업 시작 시각이 시작 날짜와 종료 날짜안에 포함되는지 확인하는 간단한 함수입니다. 비교를 위해 milliseconds로 환산한 값을 사용하고 있습니다. 하지만, checkDatePassLimit는 이런 함수의 동작을 충분히 설명하지 못합니다.

 먼저 limit은 범위가 아니라 특정 시점을 나타냅니다.

 게다가 pass는 지나가다, 통과하다라는 의미로 다음 그림과 같이 표현됩니다.

 고집을 부려 종료 날짜를 limit으로 둔 것이라 할 수 있지만, 그렇게 되면 종료 날짜를 통과할 때 true를 반환하는 것이 자연스럽습니다. Date가 한계를 통과하는지 확인하라라는 뜻이니까요. 하지만 종료 날짜를 지난다면 false를 반환해야 하기 때문에 이름을 그대로 두는 것은 문제가 됩니다.

 저희가 원하는 그림은 작업 시작 시각이 아래처럼 범위에 포함되는 것입니다.

 그렇다면 isDateWithin + 기간/범위으로 이름을 바꾸면 좋을 것 같습니다! 기간 및 범위를 표현하는 단어중엔 ‘duration’, ‘period’, ‘range’ 및 ‘interval’가 있는데, 각자 뉘앙스가 다르므로 최대한 알맞은 단어를 골라야 합니다.

 duration은 어떤 사건이 지속되거나 발생하는 시간의 길이를 말합니다. 즉, 두 지점 또는 두 이벤트 사이의 경과 시간을 측정합니다. 예를 들어, 영화의 상영 시간에 duration을 쓸 수 있습니다. 저희 상황에는 적절하지 않습니다.

 period는 기간을 가리키지만, 이벤트의 반복 또는 주기를 의미하는 경우가 많습니다. 예를 들어, 진자가 한 번 왕복하는 데 걸리는 시간을 period라고 합니다. 마찬가지로 저희 상황엔 적절하지 않습니다.

 range는 더 넓은 의미로 다양한 맥락에서 사용 가능합니다. 시간의 맥락에선 시작점과 종료점에 이르는 범위를 일컫습니다. 범위 뿐만 아니라 시작점과 종료점을 아우르는 표현이기 때문에 range가 적절한 표현입니다.

 interval은 두 사건 사이의 구간을 의미합니다. 다만, period와 비슷하게 반복되는 뉘앙스가 포함됩니다. 구간은 규칙적이거나 불규칙할 수 있으며 길이가 고정되거나 고정되지 않을 수 있습니다. 예를 들어, 월간 미팅의 interval은 한 달 이지만, 정확히 언제인지는 달마다 다를 수 있습니다. 마찬가지로 저희 상황엔 적절하지 않습니다.

 결국 isDateWithinRange가 가장 적절해 보입니다. 추가적으로 작업 시작 시각이 Date(YYYY-MM-DD)가 아닌 DateTime(YYYY-MM-DD HH-MM-SS)이므로 isDateTimeWithinRange로 바꿔주었습니다. 물론 이것이 가장 적절한 이름은 아닙니다. 하지만, 동작을 충분히 설명하기 어려운 checkDatePassLimit를 대체할 수 있었습니다.

const MILLISECONDS_IN_A_DAY = 86400000;

export const isDateTimeWithinRange = (
  startDateInMilliSec: number, // 시작 날짜 (Date to Milli Seconds)
  endDateInMilliSec: number, // 종료 날짜 (Date to Milli Seconds)
  startTaskDateTimeInMilliSec: number // 작업 시작 시각 (DateTime to Milli Seconds)
) => {
  if (
    startTaskDateTimeInMilliSec >= startDateInMilliSec &&
    startTaskDateTimeInMilliSec < endDateInMilliSec + MILLISECONDS_IN_A_DAY
  )
    return true;

  return false;
};

❗️📝 정확하지 않아도 되는 경우

 이름은 바꿨으나 함수 인자와 내용이 한 눈에 들어오지 않습니다. date 및 date time을 밀리세컨드 단위로 환산한 의미를 직관적으로 주지만 코드가 지저분해 보일 수 있습니다. 따라서, 조금 정확하지 않더라도 ‘밀리세컨드로 환산한 값’이라는 의미를 제거하도록 타협을 보면 좋겠습니다.

 다행히 이 함수 내에선 모든 값들이 밀리세컨드로 환산한 값이기 때문에 통일성을 유지한 채 이름을 수정할 수 있었습니다.

const DAY = 86400000;

export const isDateTimeWithinRange = (
  startDate: number, // 시작 날짜
  endDate: number, // 종료 날짜
  startTaskDateTime: number // 작업 시작 시각
) => {
  if (
    startTaskDateTime >= startDate &&
    startTaskDateTime < endDate + DAY
  )
    return true;

  return false;
};

 조금 욕심을 부린다면 startTaskDateTime에서 시작의 의미를 감춰도 좋을 것 같습니다. startDate와 겹치는 것이 거슬린다면 말이죠.

const DAY = 86400000;

export const isDateTimeWithinRange = (
  startDate: number, // 시작 날짜
  endDate: number, // 종료 날짜
  taskDateTime: number // 작업 시각
) => {
  if (
    taskDateTime >= startDate &&
    taskDateTime < endDate + DAY
  )
    return true;

  return false;
};

 정답은 없습니다. 다만, 정확한 표현을 쓰는 것보다 부정확한 표현을 허용함으로써 전체적인 맥락을 빠르게 파악하는데 도움을 줄 수 있습니다.

2. 잘 보이는 형태로 작성하기

🗒 표로 삼항 연산자 대체하기

 가장 먼저 표 모델을 사용한 사례를 보여드리겠습니다.

 저희 프로젝트에선 코드 내에 중첩된 삼항 연산자가 많았습니다. 다음 코드는 의도한대로 동작하지만, 직관적으로 눈에 들어오지 않았습니다.

  {
    isLoading ? (
      view === 'card' ? (
        <CardListSkeleton />
      ) : (
        <TableSkeleton />
      )
    ) : (
      <>
        {projectList.length !== 0 ? (
          view === 'card' ? (
            <CardList />
          ) : (
            <Table />
          )
        ) : (
          <Empty />
        )}
      </>
    );
  }

 내용을 요약하면 다음과 같습니다.

  • isLoading이면 Skeleton을 보여준다.
  • view가 ‘card’ 또는 ‘table’이냐에 따라 projectList(또는 Skeleton)를 다른 형태로 보여준다.
  • project가 0개면, Empty 컴포넌트를 보여준다.

 위 코드에선 삼항 연산자가 중첩된 것도 있지만, view === 'card' 조건 분기가 두번 반복되는 것을 볼 수 있습니다. 그래서 우선 중복되는 조건을 최대한 없애고 플로우 차트를 만들어 보기로 했습니다.

 다음은 실제로 pr을 올릴 때 이해를 돕고자 직접 그린 플로우 차트입니다.

 이제 조금이나마 간소화된 플로우 차트를 한눈에 파악하기 위해 표를 사용할 것입니다. 순차적으로 보지 않아도 원하는 행과 열에만 시선을 두면 인과관계를 파악할 수 있습니다.

 이제 표를 코드로 작성해보겠습니다. 훨씬 깔끔해짐과 동시에 직관적으로 파악하기 쉬워졌습니다.

편의상 컴포넌트이름에서 Project는 생략하겠습니다.

  if (projectList.length === 0) return <Empty />;

  if (view === 'card') {
    if (isLoading) return <CardListSkeleton />
    else return <CardList />
  }
  if (view === 'table') {
    if (isLoading) return <TableSkeleton />
    else return <Table />
  }

 조건문이 중첩된 것이 거슬린다면 early return을 사용하는 방법도 있습니다.

  if (projectList.length === 0) return <Empty />;

  if (view === 'card' && isLoading) return <CardListSkeleton />
  if (view === 'card') return <CardList />

  if (view === 'table' && isLoading) return <TableSkeleton />
  return <Table />

 삼항 연산자를 사용할 때보다 특정 조건과 그에 해당하는 결과물을 쉽게 파악할 수 있게 되었습니다.

🗄 용어 정리

 영상에 소개된 표식 삽입 기법도 적극적으로 사용하고 있습니다. 조건문에 들어간 표현식이 무엇을 의미하는지를 따로 변수로 빼두어 설명한다면, 다른 개발자가 봤을 때도 코드의 의도를 쉽게 파악할 수 있기 때문입니다. 영상에 나온 용어 정리 기법과는 조금 다른 내용이지만, 용어를 통일하여 개발이나 커뮤니케이션 중 발생한 혼동을 줄인 사례가 있어서 소개해드리고 싶습니다.

 저는 NER 라벨링 툴을 구현하고 있습니다.

그림 출처: https://www.shaip.com/blog/named-entity-recognition-and-its-types/

 그런데, 어느샌가 NER에서 쓰이는 단어들이 혼용되고 있었습니다.

 예를 들어, 위 사진에서 ‘LOCATION’, ‘DATE’, ‘PERSON’을 특정 파일 내에서는 Tag라고 표현하고, 다른 곳에서는 Label이라고 부르는 혼동이 있었습니다. 게다가 NER 결과를 나타낼 때에도 Tag라는 용어를 사용하고 있었습니다. 즉 한 가지 개념을 여러 용어로 표현하거나, 반대로 여러 개념을 하나의 용어로 표현하고 있었습니다.

 이러한 문제는 코드 내뿐만 아니라 기획팀과의 회의에서도 발생하였습니다. 서로 같은 개념을 다른 용어로 사용하는 경우가 있었습니다.

 이 문제를 해결하기 위해 다음처럼 용어 통일의 필요성을 제시하였습니다.

 이로 인해, 용어 혼용을 피할 수 있었고 코드가 화면상의 무엇을 지칭하는지 쉽게 파악할 수 있었습니다.

마치며

 지금까지 섬세한 ISFP의 코드 가독성 개선 경험을 참고하여 리팩토링한 사례들을 보여드렸습니다. 먼저 정확한 단어 고르기를 진행하면서는 영단어를 공부하던 학생때로 돌아간 기분이 들기도 했습니다. 하지만 이 리팩토링 작업을 수행하면서 뉘앙스 차이를 정확히 인지하는 것이 공식 문서를 파악하거나 작성한 코드의 동작을 예측 가능하게 만들어주는데 도움이 되었습니다. 잘 보이는 형태로 작성하기에선 사람이 직관적으로 이해하기 쉬운 그림 모델을 코드에 도입해봤습니다. 또한, 혼용되는 단어들을 통일함으로써 코드의 일관성을 높이고 소통에서 발생하는 혼란을 줄일 수 있었습니다.

 이번 리팩토링을 진행하면서 뉘앙스의 차이를 명확히 알고 사용한 단어와 부연 설명을 위한 그림을 통해 더 직관적으로 코드를 이해할 수 있었습니다. 분명 내가 짠 코드지만 남에게 코드를 설명할 때 코드를 다시 읽고 이해한 뒤 설명해주곤 했습니다. 하지만, 한번 시간을 들여 위와 같은 기법으로 리팩토링을 진행한다면 인수인계에 드는 시간 비용을 줄일 수 있다는 생각이 들었습니다. 그만큼 의미있는 시간이었습니다.

Reference