Toss의 Frontend Fundamentals를 읽어보다

delina·2025년 2월 27일
post-thumbnail

개발을 하면서 모든 개발자들이 그렇겠지만 늘 좋은코드란 무엇인가 고민하게됩니다. 클린코드에 대한 글들은 워낙 많아서 이미 아는 부분도 좀 있지만 아는것과 그것을 적용시키는건 일이더라구요.. 😅

요근래 신규 기능 개발건들이 휘몰아치면서 어찌저찌 개발을 끝내긴 했는데..
그러다 보니 '좋은 코드'에 대한 생각을 깊게 하지 못하고 끝나버린 느낌이라 반성겸 , 우연치않게 toss에서 제공해준 Frontend Fundamentals를 본겸 해서 상기시키기 위한 정리해보려합니다!


좋은코드란?

변경하기 쉬운 코드이다.

(이 말을 들었을때 오... 맞는말이다 라고 생각했습니다 ...
좋은 코드에 대한 말은 많지만 , 결국 개발자가 유지보수 하기 쉬운 , 변경이 쉬운 코드가 좋은코드이지 않겠어요?!ㅋㅋㅋ)

변경하기 쉬운코드란?

1. 가독성이 좋은 코드

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


이렇게 viwer라는 권한을 가지고 있을때 파란색은 보기 전용 권한 / 빨간색은 일반 사용자일때 실행되는 코드를

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>;
}

이렇게 권한별 코드를 나눠서 관리하도록 지정해주자!

구현 상세 추상화하기

  • 한 사람이 코드를 읽을 때 동시에 고려할 수 있는 총 맥락의 수는 제한되어 있다고해요.
  • 내 코드를 읽는 사람들이 코드를 쉽게 읽을 수 있도록 하기 위해 불필요한 맥락을 추상화 하면 좋아요!

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

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

예를들어

// bad👎🏻
// 페이지가 필요한 모든 쿼리 파라미터를 관리하는 훅

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]
  );
}
  • 이렇게 관리하면 Hook의 책임이 무제한적으로 늘어날 수 있습니다!
  • 또한 하나의 쿼리 파라미터가 수정되더라도 리렌더링이 발생합니다.

따라서

// good 👍🏻
import { NumberParam, 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;
}
  • 각각 쿼리 파라미터별 별도의 hook을 작성합시다!
    (결합도 관점으로도 볼 수 있죠! )

그밖에

이름붙이기

  • 복잡한 조건에 이름을 붙이기
const matchedProducts = products.filter((product) => {
  return product.categories.some((category) => {
    const isSameCategory = category.id === targetCategory.id;
    const isPriceInRange = product.prices.some(
      (price) => price >= minPrice && price <= maxPrice
    );

    return isSameCategory && isPriceInRange;
  });
});
  • 매직넘버에 이름 붙이기
const ANIMATION_DELAY_MS = 300; // function 밖에 작성하여 한번 호출되도록 하고 단위까지 깔끔 🩷

async function onLikeClick() {
  await postLike(url);
  await delay(ANIMATION_DELAY_MS);
  await refetchPostLike();
}

위에서 아래로 읽히게 하기

  • 시점 이동 줄이기 (코드를 읽을때 위아래를 왔다갔다 하면서 읽지 않도록 ! )
  • 삼항 연산자 단순하게 하기
    - 이중으로 사용하기보단 아래처럼 사용해보자!
 const status = (() => {
  if (A조건 && B조건) return "BOTH";
  if (A조건) return "A";
  if (B조건) return "B";
  return "NONE";
})();

2. 예측가능성이 높은 코드

이름 겹치지 않게 관리하기

  • 같은 이름을 가지는 함수나 변수는 동일한 동작을 해야합니다.
  • 작은 동작의 차이라도 코드의 예측 가능성을 낮추고 , 코드를 읽는 사람에게 혼란을 줄 수 있습니다!

같은 종류의 함수는 반환 타입 통일하기

  • API 호출과 관련된 Hook들처럼 같은 종류의 함수나 Hook이 서로 다른 반환타입을 가지면 코드의 일관성이 떨어져 , 코드를 읽는데 헷갈릴 수 있습니다!
import { useQuery } from '@tanstack/react-query';

function useUser() {
  const query = useQuery({
    queryKey: ['user'],
    queryFn: fetchUser
  });

  return query; // 1️⃣ 여기선 query 객체를 반환하고
}

function useServerTime() {
  const query = useQuery({
    queryKey: ['serverTime'],
    queryFn: fetchServerTime
  });

  return query.data; // 2️⃣ 여기선 data를 반환한다?!
}

➡️ 데이터 형태 통일하기 !!

숨은 로직 드러내기

  • 함수나 컴포넌트 이름,파라미터,반환 값에 드러나지 않은 숨은 로직이 있다면 다른 동료들이 동작을 예측하는데 어려움을 겪을 수 있습니다
async function fetchBalance(): Promise<number> {
  const balance = await http.get<number>("...");

  logging.log("balance_fetched"); // 👎🏻 이 함수의 이름만 가지고는 로깅이 이루어지는지 알 수 없어요 

  return balance;
}
// 이렇게 분리하기 👍🏻
const balance = await fetchBalance();
logging.log("balance_fetched");

3. 응집도가 높은 코드

함께 수정되는 파일을 같은 디렉토리에 두기

  • 이 부분은 프로젝트 구조에 따라 다르겠지만 FSD아키텍처와 비슷한 부분이 많았다!
  • 도메인 별 폴더 관리를 하자! 라는 느낌

매직 넘버 없애기

  • 위에서도 언급했던 내용!

Form의 응집도 생각하기

  • From을 관리할때 2가지 방법으로 응집도를 관리하자!
  1. 필드 단위 응집도
    • 개별 입력 요소를 독립적으로 관리하는 방식
  2. 폼 전체 단위 응집도
    • 모든 필드의 검증 로직이 폼에 종속되는 방식

어떤걸 사용하는게 좋을지에 대한 요약은 아래와 같습니다 🙃

필드 단위 응집도: 독립적인 검증(이메일, 추천 코드)이나 재사용이 필요한 경우 적합.
폼 전체 단위 응집도: 모든 필드가 하나의 기능(결제, 배송)으로 연결되거나 단계별 입력이 필요한 경우 유용.

4. 결합도가 낮은 코드

책임을 하나씩 관리하기

  • 위에서도 말했던 , 쿼리 파라미터, 상태, API 호출과 같은 로직의 종류에 따라서 함수나 컴포넌트, Hook을 나누지 마세요! 라는 내용

중복코드 허용하기

  • 흥미로운 주제가 나왔네요 😄
  • 중복 코드를 하나의 Hook이나 컴포넌트로 공통화 하는것은 좋지만, 불필요한 결합도가 생길 수 있으니 중복코드를 허용하는것도 있어야한다!
  • 상황에 따라 좋은 선택을 해야겠죠 ? ㅎㅎ

Props Drilling 지우기

  • Props Drilling대신 ``은 부모-자식 간 결합도를 높이며, prop 변경 시 모든 관련 컴포넌트를 수정해야 하는 문제를 야기해요.
  • Props Drilling대신 ContextAPI조합(Composition)패턴을 사용해보아요!
function ItemEditModal({ open, items, recommendedItems, onConfirm, onClose }) {
  const [keyword, setKeyword] = useState("");

  return (
    <Modal open={open} onClose={onClose}>
      <ItemEditBody onClose={onClose}>
        <ItemEditList
          keyword={keyword}
          items={items}
          recommendedItems={recommendedItems}
          onConfirm={onConfirm}
        />
      </ItemEditBody>
    </Modal>
  );
}

// 이런식으로 children을 사용하기 
function ItemEditBody({ children, onClose }) {
  return (
    <>
      <div style="display: flex; justify-content: space-between;">
        <Input
          value={keyword}
          onChange={(e) => onKeywordChange(e.target.value)}
        />
        <Button onClick={onClose}>닫기</Button>
      </div>
      {children}
    </>
  );
}

토스에서도 위의 큰 4가지 기준을 한꺼번에 충족하긴 어렵다고 말합니다 😅‼️

🟢 예를들어 함수나 변수가 항상 같이 수정되기위해 공통화 및 추상화를 하면 응집도는 높아짐
🔴 but 코드가 한 차례 추상화되기때문에 가독성은 떨어짐

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

따라서 , 개발자는 장기적으로 코드가 수정하기 쉽게 하기 위해서는 어떤 가치를 우선해야 하는지 고민해야합니다!


오늘 정리한 내용을 되새기며, 과거의 내 코드를 돌아보니 반성할 점도 보이네요. 😊... 쥬륵

변경하기 쉬운 코드를 만들기 위해 조금씩 더 생각하면서 코드를 짜야겠다는 생각을 했습니다.

내 동료들이 코드를 딱 봤을때 이해하기 쉽도록!!
짜보도록 노력해야겠어요!

참고적으로 frontend-fundamentals 커뮤니티에서
좋은 토론 모아보기AvsB 같은 페이지도 제공하는데 생각보다 좋은 고민들이 많이 있어 틈틈히 보면 좋을것 같다는 생각입니다!

( 제가 지금 개발 및 운영하고있는 댑댑댑 이라는 싸이트에서도 A vs B 와 비슷하게 픽픽픽 이라는 기능을 제공해주기에 여기도 한번 방문해보셔도 좋습니다 😄)

profile
Hi ! I'm Delina , FE developer

0개의 댓글