티스토리 뷰

 

toss에서 만든 지침서

 

 

좋은 코드란 어떤 코드라고 생각하세요?

 

개발자라면 개발자끼리의 대화를 할 때나, 면접에서나 아마 한 번쯤은 들어봤을 질문이라고 생각한다. 그리고 그에 대한 답변은 각양각색일 수 있다고 생각한다. 나와 같은 경우에는 면접에서 이러한 질문을 받았을 때  '유지보수 측면에서 가독성이 좋은 코드가 좋은 코드라고 생각합니다.'라고 대답했었던 기억이 있다. 지금 생각하면 구체적으로 생각해본 적이 많이 없기 때문에 근거가 부족한 애매한 답변이었다는 생각이 든다.

 

마침 토스에서 FE 지침서 만들었다는 소식을 지인에게 들었고 다음에 같은 질문을 받았을 때는 내가 내린 좋은 코드에 대한 정의를 자신있게 말할 수 있기 위해서 해당 글을 읽고 정리하고자 한다.

 

이번 포스팅에서는 가독성과 관련된 글 위주로 다룰 예정이다.


좋은 코드

토스에서는 좋은 코드를 변경하기 좋은 코드라고 소개하고 있다. 새로운 요구사항을 구현하고자 할 때, 기존 코드를 수정하고 배포하기 수월한 코드가 좋은 코드라고 한다. 코드를 변경하기 쉬운지는 4가지의 기준을 두고 판단할 수 있다.

1. 가독성

가독성은 코드를 읽기 쉬운 정도를 말한다. 코드를 변경하기 쉬우려면 먼저 코드가 어떻게 작동하는지 이해할 수 있어야 한다.

읽기 좋은 코드는 읽는 사람이 한 번에 머릿속에서 고려하는 맥락이 적고, 위에서 아래로 자연스럽게 이어져야 한다.

2. 예측 가능성

예측 가능성은 함께 협업하는 동료들이 함수나 컴포넌트의 동작을 얼마나 예측할 수 있는지를 말한다. 예측 가능성이 높은 코드는 일관적인 규칙을 따르고, 함수나 컴포넌트의 이름과 파라미터, 반환 값만 보고도 어떤 동작을 하는지 알 수 있다.

3. 응집도

응집도는 수정되어야 할 코드가 항상 같이 수정되는지를 말한다. 응집도가 높은 코드는 코드의 한 부분을 수정해도 의도치 않게 다른 부분에서 오류가 발생하지 않는다. 함께 수정해야 할 부분이 반드시 함께 수정되도록 구조적으로 뒷받침되기 때문이다.

4. 결합도

결합도란, 코드를 수정했을 때의 영향범위를 말한다.. 코드를 수정했을 때 영향범위가 적어서, 변경에 따른 범위를 예측할 수 있는 코드가 수정하기 쉬운 코드이다.


결합도와 응집도의 상관관계

그렇다면 우리는 가독성, 예측 가능성, 응집도, 결합도 이렇게 총 4가지 요소가 모두 고려된 코드를 만들기 위해서 노력하면 될 것 같습니다. 그러나 결합도와 응집도는 서로 상충되는 경우가 많다고 한다.

일반적으로 응집도를 높이기 위해서는 변수나 함수를 추상화하는 등 가독성을 떨어뜨리는 결정을 해야한다. 함께 수정되지 않으면 오류가 발생할 수 있는 경우에는, 응집도를 우선해서 코드를 공통화, 추상화하세요위험성이 높지 않은 경우에는, 가독성을 우선하여 코드 중복을 허용하자.

 

또한 결합도와 응집도를 제외하고도 위 4가지 요소를 모두 충족하기에는 어렵다. 예를 들어서, 함수나 변수가 항상 같이 수정되기 위해서 공통화 및 추상화하면, 응집도가 높아진다. 그렇지만 코드가 한 차례 추상화되기 때문에 가독성이 떨어진다.

중복 코드를 허용하면, 코드의 영향범위를 줄일 수 있어서, 결합도를 낮출 수 있다. 그렇지만 한쪽을 수정했을 때 다른 한쪽을 실수로 수정하지 못할 수 있어서, 응집도가 떨어진다.

프론트엔드 개발자는 현재 직면한 상황을 바탕으로, 깊이 있게 고민하면서, 장기적으로 코드가 수정하기 쉽게 하기 위해서 어떤 가치를 우선해야 하는지 고민해야 한다.


가독성이 높은 코드를 만들어내는 방법

1. 같이 실행되지 않는 코드 분리하기

동시에 실행되지 않는 코드가 하나의 함수 또는 컴포넌트에 있으면, 동작을 한눈에 파악하기 어려워진다. 구현 부분에 많은 숫자의 분기가 들어가서, 어떤 역할을 하는지 이해하기 어렵기도 하다.

 

아래 코드를 통해 좀 더 깊이있게 알아보도록 하자.

 

개선전

function SubmitButton() {
  const isViewer = useRole() === "viewer";

  useEffect(() => {
    if (isViewer) {
      return;
    }
    showButtonAnimation();
  }, [isViewer]);

  return isViewer ? (
    <TextButton disabled>Submit</TextButton>
  ) : (
    <Button type="submit">Submit</Button>
  );
}

 

<SubmitButton /> 컴포넌트에서는 사용자가 가질 수 있는 2가지의 권한 상태를 하나의 컴포넌트 안에서 한 번에 처리하고 있다. 그래서 코드를 읽는 사람이 한 번에 고려해야 하는 맥락이 많아진다.

 

개선후

function SubmitButton() {
  const isViewer = useRole() === "viewer";

  return isViewer ? <ViewerSubmitButton /> : <AdminSubmitButton />;
}

function ViewerSubmitButton() {
  return <TextButton disabled>Submit</TextButton>;
}

function AdminSubmitButton() {
  useEffect(() => {
    showAnimation();
  }, []);

  return <Button type="submit">Submit</Button>;
}
  • <SubmitButton /> 코드 곳곳에 있던 분기가 단 하나로 합쳐지면서, 분기가 줄어든다.
  • <ViewerSubmitButton />과 <AdminSubmitButton /> 에서는 하나의 분기만 관리하기 때문에, 코드를 읽는 사람이 한 번에 고려해야 할 맥락이 적어진다.

2. 구현 상세 추상화하기

자신의 코드를 쉽게 읽을 수 있도록 불필요한 맥락을 추상화 하는 것이 좋다.

 

아래의 예제 코드를 보면, 해당 컴포넌트는 사용자가 로그인이 되었는지를 확인하고, 로그인이 된 경우 홈으로 이동시키는 로직을 가지고 있다.

 

개선 전

function LoginStartPage() {
  useCheckLogin({
    onChecked: (status) => {
      if (status === "LOGGED_IN") {
        location.href = "/home";
      }
    }
  });

  /* ... 로그인 관련 로직 ... */

  return <>{/* ... 로그인 관련 컴포넌트 ... */}</>;
}

해당 코드는 사용자를 홈으로 이동시키는 로직이 추상화 없이 노출되어 있다. 그래서 useCheckLogin, onChecked, status 등과 같은 변수나 값을 모두 읽어야 무슨 역할을 하는 코드인지를 알 수 있다.

사용자가 로그인되었는지 확인하고 이동하는 로직을 HOC(Higher-Order Component) 나 Wrapper 컴포넌트로 분리하여, 코드를 읽는 사람이 한 번에 알아야 하는 맥락을 줄여보자. 분리된 컴포넌트 안에 있는 로직끼리 참조를 막음으로써, 코드 간의 불필요한 의존 관계가 생겨서 복잡해지는 것을 막을 수 있다.

 

개선 후


옵션 A: Wrapper 컴포넌트 사용하기

function AuthGuard({ children }) {
  const status = useCheckLoginStatus();

  useEffect(() => {
    if (status === "LOGGED_IN") {
      location.href = "/home";
    }
  }, [status]);

  return status !== "LOGGED_IN" ? children : null;
}

function LoginStartPage() {
  /* ... 로그인 관련 로직 ... */

  return <>{/* ... 로그인 관련 컴포넌트 ... */}</>;
}

function App() {
  return (
    <AuthGuard>
      <LoginStartPage />
    </AuthGuard>
  );
}

 

옵션 B: HOC 컴포넌트 사용하기

// HOC 정의
function withAuthGuard(WrappedComponent) {
  return function AuthGuard(props) {
    const status = useCheckLoginStatus();

    useEffect(() => {
      if (status === "LOGGED_IN") {
        location.href = "/home";
      }
    }, [status]);

    return status !== "LOGGED_IN" ? <WrappedComponent {...props} /> : null;
  };
}

export default withAuthGuard(LoginStartPage);

function LoginStartPage() {
  /* ... 로그인 관련 로직 ... */

  return <>{/* ... 로그인 관련 컴포넌트 ... */}</>;
}

 

3. 로직 종류에 따라 합쳐진 함수를 쪼개기

쿼리 파라미터, 상태, API 호출과 같은 로직의 종류에 따라서 함수나 컴포넌트, Hook을 만들지 않는다. 한 번에 다루는 맥락의 종류가 많아져서 이해하기 힘들고 수정하기 어려운 코드가 된다.

 

개선 전

import moment, { Moment } from "moment";
import { useMemo } from "react";
import {
  ArrayParam,
  DateParam,
  NumberParam,
  useQueryParams
} from "use-query-params";

const defaultDateFrom = moment().subtract(3, "month");
const defaultDateTo = moment();

export function usePageState() {
  const [query, setQuery] = useQueryParams({
    cardId: NumberParam,
    statementId: NumberParam,
    dateFrom: DateParam,
    dateTo: DateParam,
    statusList: ArrayParam
  });

  return useMemo(
    () => ({
      values: {
        cardId: query.cardId ?? undefined,
        statementId: query.statementId ?? undefined,
        dateFrom:
          query.dateFrom == null ? defaultDateFrom : moment(query.dateFrom),
        dateTo: query.dateTo == null ? defaultDateTo : moment(query.dateTo),
        statusList: query.statusList as StatementStatusType[] | undefined
      },
      controls: {
        setCardId: (cardId: number) => setQuery({ cardId }, "replaceIn"),
        setStatementId: (statementId: number) =>
          setQuery({ statementId }, "replaceIn"),
        setDateFrom: (date?: Moment) =>
          setQuery({ dateFrom: date?.toDate() }, "replaceIn"),
        setDateTo: (date?: Moment) =>
          setQuery({ dateTo: date?.toDate() }, "replaceIn"),
        setStatusList: (statusList?: StatementStatusType[]) =>
          setQuery({ statusList }, "replaceIn")
      }
    }),
    [query, setQuery]
  );
}

 

해당 훅은 페이지가 필요한 모든 쿼리 파라미터를 관리하는 것이다보니, 담당할 책임이 무한정 늘어날 수 있다는 단점이 있다.

 

개선 후

import { useQueryParam } from "use-query-params";

export function useCardIdQueryParam() {
  const [cardId, _setCardId] = useQueryParam("cardId", NumberParam);

  const setCardId = useCallback((cardId: number) => {
    _setCardId({ cardId }, "replaceIn");
  }, []);

  return [cardId ?? undefined, setCardId] as const;
}

 

위와 같이 코드를 개선함으로써 usePageState보다 더 명확한 이름을 가지고, 해당 훅을 수정했을 때 예상치 못한 변경이 생기는 것을 막을 수 있다!

4. 복잡한 조건에 이름 붙이기

코드로 복잡한 조건을 구현하려고 하면 자연스럽게 읽기 힘들어지고, 가독성은 떨어지게 된다.

 

개선 전

const result = products.filter((product) =>
  product.categories.some(
    (category) =>
      category.id === targetCategory.id &&
      product.prices.some(
        (price) => price >= minPrice && price <= maxPrice
      )
  )
);

 

개선 후

const matchedProducts = products.filter((product) => {
  return product.categories.some((category) => {
    const isSameCategory = category.id === targetCategory.id;
    const isPriceInRange = product.prices.some(
      (price) => minPrice <= price && price <= maxPrice
    );

    return isSameCategory && isPriceInRange;
  });
});

조건이 2개가 있었던 것을 변수에 할당하고, 조건에 네이밍을 해줌으로써 가독성을 높일 수 있다.

또한 이건 디테일하고 개인적으로 느끼는 부분인데, price >= minPrice && price <= maxPrice 보다 minPrice <= price && price <= maxPrice 이 좀 더 가독성이 좋은 느낌이다.

 

출처

Frontend Fundamentals

 

Frontend Fundamentals

Guidelines for easily modifiable frontend code

frontend-fundamentals.com

 

 

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/05   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31