본문 바로가기
개발 공부/Self Study

[MTFFM] 5일차: 복잡한 조건에 이름 붙이기

by jetproc 2026. 5. 24.
728x90

# 개발자가 가장 어려워하는 것.. 이름 짓기

우리는 복잡한 비즈니스 요구사항을 처리하다 보면, 컴포넌트 내부나 JSX 렌더링 영역에 다음과 같은 서면 형태의 조건식들을 무심코 작성하게 된다.

if (user.isLoggedIn && user.membership === 'PREMIUM' && !user.isSuspended && cart.totalPrice >= 50000) { ... }

이 코드가 화면에 그대로 노출되어 있으면 코드를 읽는 동료(혹은 미래의 나)는 이 조건식이 "무엇을 뜻하는지(의도)"를 파악하기 전에, 연산자들을 하나씩 머릿속으로 굴리며 "어떻게 계산되는지(구현 상세)"부터 먼저 해석해야만 한다.

즉, 코드를 읽는 사람에게 엄청난 인지 부하와 맥락을 강요하게 되는 것이다.

그래서 토스 Fundamentals에선 다음과 같이 말하고 있다.

필터 조건이나 혜택 대상 여부처럼 도메인 의미가 담긴 복잡한 조건식은, 의도가 명확히 드러나는 이름의 변수나 함수로 분리(추상화)하는 것이 핵심이다.
const isEligibleForFreeShipping = isLoggedIn && isPremiumMember && ...

이렇게 이름을 붙여주면 독자는 세부 연산식을 읽지 않고도 위에서 아래로 자연스럽게 의도를 파악하며 코드를 읽어 내려갈 수 있다.
조건이 도메인 용어로 설명될 수 있거나, 테스트 코드를 붙이고 싶은 규칙일 때 이 원칙은 엄청난 힘을 발휘한다고 한다.


# Before Code

아래 코드는 유저의 상태와 장바구니 조건에 따라 쿠폰 적용 가능 여부를 판단하고 화면에 렌더링 하는 컴포넌트이다.
조건식이 너무 복잡하게 얽혀 있어서 가독성이 최악..인데 이 코드를 도메인 의미가 드러나도록 이름을 붙여 리팩토링해보았다.

//🎯 리팩토링 미션
// JSX(렌더링 영역) 안에서 삼항 연산자와 함께 뒤엉켜 있는 복잡한 조건식들을 의미가 명확한 명사형 변수명으로 추출하세요.
// 조건식을 분리할 때, 유저가 쿠폰을 받을 수 있는 '공통 자격 요건(예: 로그인 여부, 블랙리스트 제외)'이 중복되고 있다면 이 역시 우아하게 결합하거나 분리해 보세요.
// 변수명을 지을 때는 토스 크루들이 바로 이해할 수 있도록 명확한 도메인 용어(Pascal/camelCase 규칙 준수)를 사용하세요.

import React from 'react';

interface User {
  isLoggedIn: boolean;
  isBlacklist: boolean;
  level: 'BRONZE' | 'SILVER' | 'GOLD';
  hasWelcomeCoupon: boolean;
}

interface Cart {
  items: {id: number; price: number; category: string}[];
  totalPrice: number;
}

interface CouponProps {
  user: User;
  cart: Cart;
}

export const CouponSelector: React.FC<CouponProps> = ({user, cart}) => {
  return (
    <div className='p-4 theme-dark'>
      {/* 🚨 문제의 조건식 영역 */}
      {user.isLoggedIn &&
      !user.isBlacklist &&
      (user.level === 'GOLD' || user.level === 'SILVER') &&
      cart.totalPrice >= 30000 &&
      cart.items.some((item) => item.category === 'FOOD') ? (
        <button className='bg-blue-500 text-white p-2 rounded'>우수 회원 식품 특별 쿠폰 적용하기</button>
      ) : user.isLoggedIn && !user.isBlacklist && user.hasWelcomeCoupon && cart.totalPrice >= 10000 ? (
        <button className='bg-green-500 text-white p-2 rounded'>신규 회원 웰컴 쿠폰 적용하기</button>
      ) : (
        <p className='text-gray-400'>적용 가능한 쿠폰이 없습니다.</p>
      )}
    </div>
  );
};

처음 이 코드를 봤을 때 체감 난이도는 꽤 높았다.
어느 단계까지 추상화를 해야 할지, 또 변수 이름은 어떻게 지을지 감이 잘 안 왔기 때문이다.
그래서 일단 && 로 연결 된 모든 조건을 변수로 분리부터 해보았다.

 const isLoggedIn = user.isLoggedIn;
 const isBlackList = user.isBlacklist;
 const hasWelcomeCoupon = user.hasWelcomeCoupon;
 const isLevelGoldOrSliver = user.level === 'GOLD' || user.level === 'SILVER';
 const isValidUser = isLoggedIn && !isBlackList;

 const isOverPrice = (price: number) => cart.totalPrice > price;
 const isCategoryExist = (category: string) => cart.items.some((item) => item.category === category);

 return (
    <div className='p-4 theme-dark'>
      {isValidUser && isLevelGoldOrSliver && isOverPrice(30000) && isCategoryExist('FOOD') ? (
        <button className='bg-blue-500 text-white p-2 rounded'>우수 회원 식품 특별 쿠폰 적용하기</button>
      ) : isValidUser && hasWelcomeCoupon && isOverPrice(10000) ? (
        <button className='bg-green-500 text-white p-2 rounded'>신규 회원 웰컴 쿠폰 적용하기</button>
      ) : (
        <p className='text-gray-400'>적용 가능한 쿠폰이 없습니다.</p>
      )}
    </div>
 );

해당 코드로 중간 피드백을 받았는데 다음과 같은 답변을 받았다.

  1. 최종 도메인 목적(의도)에 이름 붙이기: JSX 내부의 삼항 연산자 조건문 자체를 아예 명확한 명사형 변수명(예: canApplySpecialVipCoupon, canApplyWelcomeCoupon)으로 한 번 더 감싸서 정의해 주면 어떨까요? 그렇게 되면 JSX 영역은 서술형 문장처럼 완벽하게 "무엇을 하는지"만 드러나게 됩니다.
  2. 함수 호출의 인지 부하 줄이기: 현재 isOverPrice(30000)나 isCategoryExist('FOOD') 같은 헬퍼 함수들은 파라미터를 동적으로 넘길 수 있어 유연하지만, 읽는 사람 입장에서는 30000이나 'FOOD'라는 매직 넘버/스트링의 맥락을 또 파악해야 합니다. 이 값들을 포함한 최종 조건 자체에 도메인 네이밍(예: isFoodCategoryIncluded, isCartAmountOver30000)을 부여하면 가독성이 어떻게 달라질지 고민해 보세요.

그렇다. 난 isOverPrice, isCategoryExist처럼 아주 습관적으로 중복 로직(처럼 보이는 것)을 묶어서 인자로 전달하는 유틸 함수를 만들고 있었다.

## 무조건적인 중복 제거가 아니라, 현재 변경 가능성과 위험을 기준으로 선택해야 한다.

2일 차에서도 했다시피 맹목적 공통화는 좋지 않은 패턴이다.
아마 함수형프로그래밍스터디를 진행하며 '함수 이름에 있는 암묵적 인자는 제거하라'라는 커맨드가 은연중에 작동했던 것 같다.

그래도 피드백에서의 예시(isFoodCategoryIncluded, isCartAmountOver30000)는 유연하지 않은 코드라고 생각하여 다시 한번 이 부분에 대해서 힌트를 요청했다.


### 멘토의 답변

"인자로 받아서 재사용성을 높이는 게 함수 본연의 역할인데, 왜 굳이 상수로 다 찢어서 중복을 만들어야 하지?"라는 의문이 드는 건 지극히 당연하고, 클린 코드를 고민하는 개발자라면 반드시 거쳐 가야 하는 핵심 질문이야.
여기에 대해 Frontend Fundamentals 문서의 핵심 철학이자, 토스 크루들이 가장 중요하게 생각하는 "가독성과 변경 가능성"의 관점에서 답을 줄게.

💡 중복 제거(공통화) vs 가독성(시점 이동 줄이기)

인자를 받는 함수(isOverPrice(30000))는 기술적 재사용성은 높여주지만, 코드를 읽는 사람에게 다음과 같은 두 가지 비용을 요구해.

  1. 시점 이동과 뇌내 연산: 코드를 읽다가 isOverPrice(30000)를 만나면, 컴포넌트 상단으로 눈을 올려 이 함수의 내부 구현(cart.totalPrice > price)을 확인한 뒤, 다시 30000이라는 숫자를 대입해서 "아, 3만 원 이상이구나"라고 머릿속으로 한 번 더 해석해야 해.
  2. 도메인 맥락의 은폐: 기획서에는 30000이 단순한 숫자가 아니라 '우수 회원 쿠폰 적용 최소 금액'이라는 뚜렷한 도메인 의미를 가지고 있어. 인자 방식으로 넘기면 이 숫자가 왜 하필 3만 원인지 그 '의도'가 이름에 담기지 못하지.

만약 내일 기획이 바뀌어서 "우수 회원 쿠폰은 장바구니 금액 3만 원 기준인데, 웰컴 쿠폰은 장바구니 금액 기준이 아니라 멤버십 가입 기간 기준으로 변경해 주세요"라고 한다면 어떻게 될까? isOverPrice라는 공통 함수는 더 이상 쓸모가 없어지거나, 내부 분기가 늘어나면서 오히려 결합도가 높아지게 돼.

"무조건적인 중복 제거가 아니라, 현재 변경 가능성과 위험을 기준으로 선택해야 한다."

지금 단계에서는 무리한 함수 공통화보다, 코드가 위에서 아래로 자연스럽게 도메인 문장처럼 읽히는 가독성을 선택하는 것이 훨씬 변경하기 쉬운 코드가 된다는 뜻이야.


충분히 납득이 되었고 난 다음과 같이 상수 분리와 함께 다음과 같은 코드로 리팩터링 할 수 있었다.


# After Code

const MIN_TOTAL_PRICE_FOR_VIP = 30000;
const MIN_TOTAL_PRICE_FOR_WELCOME = 10000;

const isLoggedIn = user.isLoggedIn;
const isBlackList = user.isBlacklist;
const hasWelcomeCoupon = user.hasWelcomeCoupon;
const isLevelGoldOrSilver = user.level === 'GOLD' || user.level === 'SILVER';
const isValidUser = isLoggedIn && !isBlackList;

const isCartAmountOverVipThreshold = cart.totalPrice > MIN_TOTAL_PRICE_FOR_VIP;
const isCartAmountOverWelcomeThreshold = cart.totalPrice >= MIN_TOTAL_PRICE_FOR_WELCOME;
const isFoodCategoryIncluded = cart.items.some((item) => item.category === 'FOOD');

const canApplySpecialVipCoupon =
isValidUser && isLevelGoldOrSilver && isCartAmountOverVipThreshold && isFoodCategoryIncluded;

const canApplyWelcomeCoupon = isValidUser && hasWelcomeCoupon && isCartAmountOverWelcomeThreshold;

return (
  <div className='p-4 theme-dark'>
    {canApplySpecialVipCoupon ? (
    <button className='bg-blue-500 text-white p-2 rounded'>우수 회원 식품 특별 쿠폰 적용하기</button>
      ) : canApplyWelcomeCoupon ? (
    <button className='bg-green-500 text-white p-2 rounded'>신규 회원 웰컴 쿠폰 적용하기</button>
      ) : (
    <p className='text-gray-400'>적용 가능한 쿠폰이 없습니다.</p>
      )}
  </div>
);

이런 식으로 코드를 작성했을 때 장점은 다음과 같다.

 

  • 완벽하게 격리된 JSX 가독성: 삼항 연산자 내부에 존재하던 그 복잡한 조건 연산들이 전부 없어졌다. 이제 JSX 영역은 canApplySpecialVipCoupon과 canApplyWelcomeCoupon이라는 비즈니스 요구사항(의도) 그 자체만 명확히 드러내며 위에서 아래로 물 흐르듯 읽힌다.
  • 매직 넘버의 의미 있는 자산화: 기존의 30000과 10000이라는 정적 숫자들이 상수 도메인 자산으로 격상되었다. 덕분에 숫자의 중복이 제거됨과 동시에, 나중에 최소 금액 기준이 바뀌더라도 상단의 변수만 고치면 되는 강력한 응집도를 확보했다.
  • 영리한 중복의 허용: 중복을 함수로 묶는 대신, 선언형 상수로 각각 분리했다. 이 덕분에 독자는 시점 이동을 완전히 최소화한 채 코드를 순차적으로 해석할 수 있게 되었다.

 

습관적으로 몸에 배어있던 중복 코드를 묶는 행동을 다시 점검할 수 있는 실습이었다.

 

728x90