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

[MTFFM] 7일차: 삼항 연산자 단순하게 하기

by jetproc 2026. 5. 28.
728x90

오늘은 가독성 파트의 여섯 번째 주제인 '삼항 연산자 단순하게 하기'를 공부했다.

리액트에서 JSX를 작성하다 보면 삼항 연산자를 자주 쓰게 된다.  
삼항 연산자는 간단한 조건에서는 꽤 직관적이다.

{isEditing ? <EditForm /> : <ViewMode />}

문제는 조건이 하나씩 늘어날 때다.

로딩이면 로딩 UI, 에러면 에러 UI, 데이터가 없으면 빈 상태 UI, 그 외에는 리스트 UI.
이런 식으로 분기가 3개, 4개가 되면 삼항 연산자는 금방 읽기 어려운 코드가 된다.

즉, 삼항 연산자는 가지 하나를 고를 때만 단순하다.

조건이 여러 개가 되는 순간, JSX는 화면 구조를 보여주는 곳이 아니라 ?와 : 짝을 해석해야하는 인지 부하가 심한 곳으로 바뀐다.


# Before code

이번에 리팩터링 할 코드는 이런 형태였다.

// 🎯 리팩토링 미션
// JSX 내부의 꼬여 있는 중첩 삼항 연산자를 해체하세요.
// 컴포넌트 상단에서 조기 리턴(Early Return)을 활용하거나, 상태별 UI를 별도의 함수/컴포넌트로 분리하여 PaymentMethodSelector 본체 컴포넌트의 메인 JSX 레이아웃이 한눈에 들어오도록 만드세요.

import React from 'react';

interface PaymentMethod {
  id: string;
  name: string;
  isPrimary: boolean;
}

interface PaymentSelectorProps {
  isLoading: boolean;
  error: Error | null;
  methods: PaymentMethod[];
  onSelect: (id: string) => void;
}

export const PaymentMethodSelector: React.FC<PaymentSelectorProps> = ({isLoading, error, methods, onSelect}) => {
  return (
    <div className='p-4 border rounded-lg bg-gray-50'>
      <h2 className='text-md font-bold mb-3'>결제 수단 선택</h2>

      {/* 🚨 지옥의 중첩 삼항 연산자 영역 */}
      {isLoading ? (
        <div className='text-center py-4 text-gray-500'>결제 수단을 불러오는 중입니다...</div>
      ) : error ? (
        <div className='text-center py-4 text-red-500'>오류가 발생했습니다: {error.message}</div>
      ) : methods.length === 0 ? (
        <div className='text-center py-4 text-gray-400'>등록된 결제 수단이 없습니다. 먼저 등록해 주세요.</div>
      ) : (
        <ul className='space-y-2'>
          {methods.map((method) => (
            <li key={method.id} className='p-3 bg-white border rounded flex justify-between items-center'>
              <span>
                {method.name} {method.isPrimary && <span className='text-xs text-blue-500 font-bold'>(기본)</span>}
              </span>
              <button
                onClick={() => onSelect(method.id)}
                className='bg-blue-500 text-white text-xs px-3 py-1 rounded hover:bg-blue-600'
              >
                선택
              </button>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
};

처음 봤을 땐 (이전과 마찬가지로) 생각보다 간단해 보여 가장 먼저 분기문을 모두 early return으로 분리했다.

  const isPaymentMethodEmpty = methods.length === 0;

  if (isLoading) {
    return <div className='text-center py-4 text-gray-500'>결제 수단을 불러오는 중입니다...</div>;
  }

  if (error) {
    return <div className='text-center py-4 text-red-500'>오류가 발생했습니다: {error.message}</div>;
  }

  if (isPaymentMethodEmpty) {
    return <div className='text-center py-4 text-gray-400'>등록된 결제 수단이 없습니다. 먼저 등록해 주세요.</div>;
  }

  return (
    <div className='p-4 border rounded-lg bg-gray-50'>
      <h2 className='text-md font-bold mb-3'>결제 수단 선택</h2>
      <ul className='space-y-2'>
        ...
      </ul>
    </div>
  );
};

이렇게 하니 중첩 삼항은 확실히 사라졌다.
위에서 예외 케이스를 하나씩 걸러내고, 마지막에 정상 케이스만 렌더링하니 읽기 훨씬 편했다.


## 놓친 점

그런데 여기서 하나 놓친 점이 있었다.

로딩, 에러, 빈 상태일 때는 바깥의 카드 레이아웃과 제목까지 사라진다.
원래 코드는 어떤 상태든 결제 수단 선택이라는 타이틀과 회색 박스는 유지되고, 내부 내용만 바뀌는 구조였다.

즉, 가독성을 높이려고 early return을 썼는데 return문이 부모 컴포넌트 바깥으로 나오면서 화면 구조가 달라지는 문제가 생긴 것이다.

이때 느낀 점은 이거였다.

early return은 좋지만, 어디에서 early return 할지까지 같이 봐야 한다.

컴포넌트 전체를 갈아엎어도 되는 상황이면 상위 컴포넌트에서 early return해도 된다.
하지만 공통 레이아웃은 유지하고 내부 콘텐츠만 바뀌어야 한다면, 콘텐츠 영역을 따로 분리한 안에서 early return 하는편이 낫다고 생각했다.


## 고민한 점

이번에 가장 고민한 부분은 두 가지였다.

첫 번째는 methods.length === 0이라는 조건문 안에 그대로 쓸지, 상수로 분리할지였다.

const isPaymentMethodEmpty = methods.length === 0;

자연스러운 고민이었고, 피드백 내용을 읽으면 알 수 있다시피 별도의 상수로 빼내는 게 맞았다.

이 부분에서 습득한 '복잡한 조건에 이름 붙이기' 지식이 체화된 것을 느낄 수 있었다.


두 번째 고민은 “언제 조기 리턴을 쓰고, 언제 단순 삼항 연산자를 그리고 언제 && 연산자를 써야 할까?”였다.

내가 정리한 기준은 다음과 같다. (언제나 나만의 기준을 세우는 것이 중요하다고 생각한다.)

1. 조건이 3개 이상이고, 로딩/에러/빈 상태처럼 예외 흐름을 먼저 제거해야 한다면  early return이 좋다.

if (isLoading) {
  return <div className='text-center py-4 text-gray-500'>결제 수단을 불러오는 중입니다...</div>;
}

if (error) {
  return <div className='text-center py-4 text-red-500'>오류가 발생했습니다: {error.message}</div>;
}

 

2. 반대로 조건이 딱 2개이고, 버튼 문구나 작은 UI 조각만 바뀐다면 삼항 연산자가 더 자연스럽다.

{isSubscribed ? '구독 취소하기' : '구독하기'}

 

3. 그리고 참일 때만 보여주면 되는 경우에는 &&가 더 읽기 쉽다.

{method.isPrimary && <span className='text-xs text-blue-500 font-bold'>(기본)</span>}

 

결국 중요한 삼항 연산자를 쓰지 말자 아니었다.
삼항 연산자가 적합한 크기를 넘었을 다른 도구를 쓰자는 것이었다.


# After code

최종적으로는 외곽 레이아웃은 PaymentMethodSelector 유지하고, 내부 콘텐츠 분기만 PaymentMethodContent 분리했다.

import React from 'react';

interface PaymentMethod {
  id: string;
  name: string;
  isPrimary: boolean;
}

interface PaymentSelectorProps {
  isLoading: boolean;
  error: Error | null;
  methods: PaymentMethod[];
  onSelect: (id: string) => void;
}

const PaymentMethodContent: React.FC<PaymentSelectorProps> = ({
  isLoading,
  error,
  methods,
  onSelect,
}) => {
  const isPaymentMethodEmpty = methods.length === 0;

  if (isLoading) {
    return <div className='text-center py-4 text-gray-500'>결제 수단을 불러오는 중입니다...</div>;
  }

  if (error) {
    return <div className='text-center py-4 text-red-500'>오류가 발생했습니다: {error.message}</div>;
  }

  if (isPaymentMethodEmpty) {
    return <div className='text-center py-4 text-gray-400'>등록된 결제 수단이 없습니다. 먼저 등록해 주세요.</div>;
  }

  return (
    <ul className='space-y-2'>
      {methods.map((method) => (
        <li key={method.id} className='p-3 bg-white border rounded flex justify-between items-center'>
          <span>
            {method.name}{' '}
            {method.isPrimary && <span className='text-xs text-blue-500 font-bold'>(기본)</span>}
          </span>
          <button
            onClick={() => onSelect(method.id)}
            className='bg-blue-500 text-white text-xs px-3 py-1 rounded hover:bg-blue-600'
          >
            선택
          </button>
        </li>
      ))}
    </ul>
  );
};

export const PaymentMethodSelector: React.FC<PaymentSelectorProps> = ({
  isLoading,
  error,
  methods,
  onSelect,
}) => {
  return (
    <div className='p-4 border rounded-lg bg-gray-50'>
      <h2 className='text-md font-bold mb-3'>결제 수단 선택</h2>
      <PaymentMethodContent
        isLoading={isLoading}
        error={error}
        methods={methods}
        onSelect={onSelect}
      />
    </div>
  );
};

# 배운 점

early return은 예외 흐름을 위에서 빠르게 제거해서 정상 흐름을 읽기 쉽게 만든다.
하지만 주의할 점은 컴포넌트 전체를 리턴해버리면 공통 레이아웃까지 사라질 수 있다.

그래서 "eraly return을 써야 한다."보다 더 중요한 질문은 이거였다.

어느 범위에서 early return 할 것인가?

1. 전체 화면이 바뀌어야 하면) 부모 컴포넌트에서 early return 한다.
2. 공통 레이아웃은 유지하고 내부만 바뀌어야 하면) 내부 콘텐츠 컴포넌트를 분리해서 그 안에서 early return 한다.

그리고 조건을 상수로 분리할 때도 이름이 정말 중요하다는 걸 다시 느꼈다.
methods.length === 0을 isPaymentMethodEmpty로 바꾸면 읽는 사람은 배열 길이를 계산하지 않고 바로 의도를 이해할 수 있다.
하지만 이름과 조건이 반대로 가면 오히려 더 헷갈린다.

오늘의 결론은 이렇다.

복잡한 분기는 JSX에서 숨기고, 단순한 선택만 JSX에 남긴다.

삼항 연산자 자체가 나쁜 건 아니다.
다만 JSX 안에서 여러 상태를 한 번에 처리하려고 하면 화면 구조보다 조건 해석이 먼저 보인다.

좋은 JSX 화면의 구조가 먼저 읽히고, 분기 로직은 필요한 만큼만 드러나는 코드인 같다.

728x90