프론트엔드 개발자도 디자인패턴 알아야할까요? - 상태패턴편

타락한스벨트전도사·2025년 6월 12일
post-thumbnail

기획자: "아, 그 회원가입 플로우에서 본인인증을 좀 더 앞쪽으로 옮겨야 할 것 같아요."

개발자: "네? 그럼 전체 단계를 다시 짜야 하는데요..."

기획자: "그리고 직장인이랑 프리랜서 가입 과정도 좀 다르게 해야겠어요. 아, 그리고 중간에 추가 서류 업로드 단계도 넣어주세요."

개발자: "😭"


이런 상황, 경험해보신 적 있나요?

처음에는 단순한 3단계 가입 플로우였는데, 기획이 바뀔 때마다 코드가 점점 복잡해지고... 결국 if/else 분기문이 모든 부분에 퍼지는 상태가 되어버리는 거죠.

이런 걸 해결하려고 디자인패턴이 있어요

사실 이런 문제를 우아하게 해결하려고 디자인패턴이라는 게 고안되었거든요. 복잡한 로직과 자주 바뀌는 요구사항에 대처하는 선배 개발자들의 지혜 같은 거죠.

"그런데 그거 백엔드에서나 쓰는 거 아닌가요?"

당신은 이미 쓰고 있을지도 모릅니다. React의 컴포넌트 시스템, Redux의 상태 관리, 각종 Hook들... 우리가 매일 쓰는 라이브러리들이 디자인패턴으로 만들어져 있어요.

상태패턴으로 복잡한 플로우 정리하기

오늘은 상태패턴을 살펴보려고 해요. 특히 복잡한 다단계 플로우에서 어떻게 활용할 수 있는지 알아보겠습니다.

한 번 상황을 가정해볼게요. 사용자가 어떤 서비스에 가입하는 플로우인데, 입력하는 정보에 따라 다음 단계가 달라지는 거예요. 그리고 기획자는 계속 "이 조건일 때는 저 단계로 보내주세요"라고 하고...

이런 상황에서 상태패턴이 어떻게 우리를 구원해줄 수 있는지, 실제 코드와 함께 살펴보겠습니다.

본론1: 상태패턴 맛보기

상태에 따라 다르게 동작하는 것들

상태에 따라 다르게 동작하는 것들은 상태패턴으로 하는 게 좋아요.

여러분도 의식하지 않고 이미 구현하고 있는 게 있어요. 바로 토글입니다.

const [isOn, setIsOn] = useState(false);

const handleClick = () => {
  if (isOn) {
    // OFF로 변경
    setIsOn(false);
  } else {
    // ON으로 변경  
    setIsOn(true);
  }
};

상태가 2개니까 괜찮아요. 하지만 다단계 가입 플로우는 어떨까요?

"다음" 버튼은 계속 동일하게 노출되어 있는데, 유저가 뭘 선택하고 어떤 단계를 거쳤느냐에 따라 다른 단계로 보내야 해요:

const handleNext = () => {
  if (step === 1) {
    setStep(2);
  } else if (step === 2 && userJob === 'employee') {
    setStep(3);
  } else if (step === 2 && userJob === 'freelancer') {
    setStep(4);
  } else if (step === 3) {
    submitData();
  }
  // ... 계속 늘어남
};

이런 식으로 분기문이 폭발하죠.

상태패턴의 핵심 아이디어

상태라는 것이 단순한 값이 아니라, 동작까지 여기로 위임시켜버리는 게 상태패턴입니다.

// 상태 = 값 + 동작
const states = {
  OFF: {
    label: 'OFF',
    handleClick: (setState) => {
      console.log('전원을 켭니다');
      setState(states.ON);  // 동작 자체에서 상태도 바꿈
    }
  },
  ON: {
    label: 'ON',
    handleClick: (setState) => {
      console.log('전원을 끕니다');
      setState(states.OFF);  // 동작 자체에서 상태도 바꿈
    }
  }
};

function ToggleButton() {
  const [currentState, setCurrentState] = useState(states.OFF);
  
  const handleClick = () => {
    // 그냥 현재 상태에서 호출하면 됨
    currentState.handleClick(setCurrentState);
  };
  
  return <button onClick={handleClick}>{currentState.label}</button>;
}

이제 handleClick에는 분기문이 없어요. 현재 상태가 알아서 동작도 하고, 다음 상태로의 전환도 처리하니까요.

위임의 힘

핵심은 위임이에요. 기존에는 메인 함수에서 모든 상태를 확인하고 처리했다면:

기존: this.handleClick() { if (상태1) {...} else if (상태2) {...} }

상태패턴에서는 현재 상태에게 처리를 맡겨버려요:

패턴: this.handleClick() { this.currentState.handleClick() }

이렇게 하면 상태별 로직이 각 상태 객체 안에 깔끔하게 정리되고, 메인 로직은 단순해지죠.

언제 써야 할까요?

"한 동작이 상태에 따라 다르게 처리되는 상황"이면 상태패턴을 고려해보세요:

  • 같은 버튼인데 상태별로 다른 일을 해야 할 때
  • 복잡한 플로우에서 단계별로 다른 처리가 필요할 때
  • if/else 분기문이 여러 곳에 퍼져있을 때

특히 복잡한 플로우에서는 각 단계가 자신의 다음 단계와 동작을 알고 있어서 더욱 유용해요. 다음에는 실제로 복잡한 플로우에서 이 패턴이 어떻게 빛을 발하는지 살펴보겠습니다.

본론2: 백엔드와 똑같이 설계할 수 없는 이유

백엔드는 깔끔하게 나누면 끝

적금 상품 API를 만든다고 생각해보세요. 아주 깔끔하죠:

// 정기적금
POST /api/savings/regular
{
  name: "김철수",
  monthlyAmount: 500000,
  period: 12
}

// 자유적금  
POST /api/savings/flexible
{
  name: "이영희", 
  targetAmount: 10000000,
  depositMethod: "automatic"
}

// ISA 계좌
POST /api/savings/isa
{
  name: "박민수",
  age: 28,
  income: 50000000,
  hasExistingISA: false
}

각 상품별로 필요한 데이터만 받으면 끝이에요. 깔끔하고 명확하죠.

프론트엔드는 똑같이 설계할 수 없어요

그런데 프론트엔드에서 이걸 그대로 따라하면 어떻게 될까요?

사용자에게 이렇게 보여주는 거죠:

┌─────────────────────────────────┐
│        적금 상품 선택하기          │
├─────────────────────────────────┤
│ □ 정기적금 (매달 일정금액)         │
│ □ 자유적금 (자유롭게 입금)         │
│ □ 정기예금 (목돈 한번에)          │
│ □ 월급통장연계적금               │
│ □ ISA 계좌                     │
│ □ 연금저축                     │
│ □ 주택청약종합저축               │
│ □ 소액투자상품연계적금            │
└─────────────────────────────────┘

사용자 심리:

  • "ISA가 뭐지? 나한테 맞나?"
  • "월급통장연계는 뭔 차이지?"
  • "정기적금이랑 정기예금 차이가 뭐지?"

결과: 선택 부담 → 페이지 이탈

유저를 붙잡아두려면 코드가 지저분해져야 해요

그래서 진입점을 최대한 줄이고, 먼저 선택하게 하지 말고 이런 전략을 써야 해요:

퍼널(Funnel): 복잡한 프로세스를 여러 단계로 나누어서 사용자를 단계별로 유도하는 방식

1단계: 부담 없는 시작

┌─────────────────────────────────┐
│     돈 모으기 시작하기            │
│                               │
│  목표 금액: [_________]원         │
│  언제까지: [_____]개월 후         │
│                               │
│         [다음]                 │
└─────────────────────────────────┘

2단계: 자연스러운 분기

┌─────────────────────────────────┐
│    어떤 방식으로 모으실건가요?     │
│                               │
│  ○ 매달 일정 금액씩             │
│  ○ 있을 때마다 자유롭게          │
│  ○ 목돈으로 한번에              │
│                               │
│         [다음]                 │
└─────────────────────────────────┘

3단계: 조건부 추가 정보

┌─────────────────────────────────┐
│      세금 혜택도 받고 싶나요?     │
│                               │
│  ○ 네 (나이/소득 확인 필요)       │
│  ○ 아니요                      │
│                               │
│         [다음]                 │
└─────────────────────────────────┘

그리고 회원가입도 전략적으로: 먼저 회원가입시키는 게 아니라 일단 기본정보 입력하고 나서 회원가입을 유도하면, 사용자는 입력했던 노력을 생각해서 이탈하지 않아요.

여기서 분기가 폭발합니다

다음 버튼이 상태에 따라 어디로 갈지, 이전에 입력했던 값에 따라 어디로 갈지 달라지겠죠?

const handleNext = () => {
  if (step === 1) {
    setStep(2);
  } else if (step === 2) {
    // 저축 방식에 따라 다른 단계로
    if (savingType === 'regular') {
      setStep(3); // 정기적금 상세 설정
    } else if (savingType === 'flexible') {
      setStep(4); // 자유적금 상세 설정  
    } else if (savingType === 'lump-sum') {
      setStep(5); // 예금 상세 설정
    }
  } else if (step === 3) {
    // 세금 혜택 여부에 따라 분기
    if (wantsTaxBenefit && age < 30) {
      setStep(6); // ISA 안내
    } else if (wantsTaxBenefit && age >= 30) {
      setStep(7); // 연금저축 안내
    } else {
      setStep(8); // 일반 적금
    }
  }
  // ... 계속 늘어남
};

그리고 이전 단계도 복잡:

const handlePrev = () => {
  if (step === 3 || step === 4 || step === 5) {
    setStep(2); // 저축 방식 선택으로
  } else if (step === 6 || step === 7 || step === 8) {
    setStep(3); // 세금 혜택 선택으로
  }
  // ... 역시 복잡
};

결국 프론트엔드가 고생합니다 😭

백엔드: 깔끔한 API 설계
프론트엔드: 사용자 이탈 방지를 위한 복잡한 플로우 관리

이 딜레마를 해결하는 게 상태패턴입니다. 다음에는 이 복잡한 플로우를 상태패턴으로 어떻게 우아하게 정리할 수 있는지 살펴보겠어요.

본론3: 상태패턴으로 퍼널 정리하기

분기를 상태로 바꾸기

본론2에서 봤던 복잡한 적금 퍼널을 기억하시나요? 이걸 상태패턴으로 어떻게 정리할 수 있을까요?

먼저 모든 상태들을 미리 정의해두겠습니다:

// 모든 퍼널 상태들을 미리 정의
const 목표금액입력 = {
  component: GoalAmountStep,
  getNextState: () => {
    // 항상 다음 단계로
    return 저축방식선택;
  }
};

const 저축방식선택 = {
  component: SavingTypeStep,
  getNextState: () => {
    // 내부 상태 보고 다음 단계 결정
    const savingType = getUserAnswer('savingType');
    if (savingType === 'regular') return 정기적금설정;
    if (savingType === 'flexible') return 자유적금설정;
    return 목돈예금설정;
  }
};

const 정기적금설정 = {
  component: RegularSavingStep,
  getNextState: () => {
    // 설정 완료 후 다음 단계로
    return 세금혜택질문;
  }
};

// ... 다른 상태들도 동일한 패턴

기존 방식 vs 상태패턴 방식:

// 기존: 분기문 지옥
if (step === 2) {
  if (savingType === 'regular') setStep(3);
  else if (savingType === 'flexible') setStep(4);
  else if (savingType === 'lump-sum') setStep(5);
}

// 상태패턴: 각 상태가 자신의 다음 단계를 안다
const 저축방식선택 = {
  getNextState: () => {
    // 내부 상태 보고 다음 단계 결정
    const savingType = getUserAnswer('savingType');
    if (savingType === 'regular') return 정기적금설정;
    return 다른상태들;
  }
};

다이어그램으로 설계하기

적금 퍼널의 복잡한 분기를 상태 다이어그램으로 표현하면 이렇게 됩니다:

각 상태가 독립적이고, 자신의 다음 상태를 결정할 수 있어요.

모든 상태를 글로벌하게 관리하기

이제 모든 상태들을 하나의 객체로 관리해요:

// 모든 상태들을 글로벌하게 관리
const funnelStates = {
  목표금액입력,
  저축방식선택,
  정기적금설정,
  자유적금설정,
  목돈예금설정,
  세금혜택질문,
  ISA계좌안내,
  연금저축안내,
  일반적금안내,
  회원가입유도,
  완료단계
};

React Hook으로 구현하기

퍼널 전용 React Hook을 만들어서 사용해요:

// 퍼널 전용 React Hook
const useFunnelState = () => {
  const [currentState, setCurrentState] = useState(funnelStates.목표금액입력);
  const [userAnswers, setUserAnswers] = useState({
    goalAmount: null,
    savingType: null,
    wantsTaxBenefit: null,
    age: null
  });
  
  const handleNext = () => {
    const nextState = currentState.getNextState();
    setCurrentState(nextState);
  };
  
  const handlePrev = () => {
    const prevState = currentState.getPrevState();
    setCurrentState(prevState);
  };
  
  return { currentState, handleNext, handlePrev };
};

메인 컴포넌트 단순하게 만들기

이제 메인 퍼널 컴포넌트는 custom hook을 사용해서 더욱 단순해져요:

function SavingsFunnel() {
  const { currentState, handleNext, handlePrev } = useFunnelState();
  
  return (
    <div>
      <currentState.component />
      <button onClick={handlePrev}>이전</button>
      <button onClick={handleNext}>다음</button>
    </div>
  );
}

분기문이 완전히 사라졌어요! 각 상태가 자신의 로직을 알고 있으니까요.

새로운 단계 쉽게 추가하기

새로운 단계를 추가하려면? 새로운 상태 객체만 만들고, 관련 상태들의 연결만 수정하면 돼요:

const 추가인증단계 = {
  component: VerificationStep,
  getNextState: () => {
    return funnelStates.완료단계;
  },
  getPrevState: () => {
    return funnelStates.회원가입유도;
  }
};

// 기존 상태의 연결만 수정
회원가입유도.getNextState = () => funnelStates.추가인증단계;

기존 handleNext, handlePrev 함수는 전혀 건드릴 필요가 없어요.

이게 상태패턴의 진짜 매력입니다. 복잡한 분기 로직을 각 상태로 분산시켜서, 전체적으로는 단순하고 확장 가능한 구조를 만드는 거예요.

본론4: 상태패턴의 다양한 활용처

에디터에서 툴 상태 관리하기

그림 에디터를 생각해보세요. 사용자는 항상 똑같은 동작을 해요:

  • 마우스 버튼 누르기
  • 끌기
  • 떼기

하지만 현재 선택된 에 따라 결과가 완전히 달라지죠:

  • 브러시 툴: 누르고 끌면 선이 그려짐 → 떼면 그리기 완료, 기본 선택 툴로 변경
  • 지우개 툴: 누르고 끌면 지워짐 → 떼면 지우기 완료, 기본 선택 툴로 변경
  • 선택 툴: 누르고 끌면 선택 영역 생성 → 떼면 선택 완료, 선택 툴 유지
  • 도형 툴: 누르고 끌면 미리보기 → 떼면 도형 생성 후 기본 선택 툴로 변경

특히 주목할 점은 툴 자동 변경이에요. 도형을 그리고 나면 자동으로 기본 툴로 바뀌는 게 UX적으로 자연스럽거든요. 이런 로직까지 각 툴 상태가 스스로 관리하는 거죠.

실제로 tldraw 같은 유명한 오픈소스 화이트보드 라이브러리에서도 tool을 상태로 관리해요. 각 툴이 자신만의 마우스 이벤트 처리 로직과 상태 전환 로직을 가지고 있죠.

똑같은 마우스 이벤트인데 툴마다 완전히 다른 처리가 필요해요. 이걸 하나의 함수에서 분기문으로 처리한다면? 툴이 추가될 때마다 지옥이 되겠죠.

상태패턴을 쓰면 각 툴이 자신의 동작을 캡슐화해서, 메인 로직은 단순히 "현재 툴에게 위임"만 하면 돼요.

XState로 상태패턴 쉽게 만들기

XState는 이런 상태패턴을 쉽게 작성해주는 라이브러리예요.

특히 좋은 점은:

  • 코드 작성 → 다이어그램 자동 생성
  • 다이어그램 작성 → 코드 자동 생성
  • 웹 에디터 서비스 제공으로 시각적으로 상태 설계 가능

복잡한 상태 전환을 눈으로 보면서 설계할 수 있어서, 놓치기 쉬운 예외 상황들도 미리 발견할 수 있어요.

웹소켓 연결 상태 관리하기

프론트엔드라면 한 번쯤 만나게 되는 웹소켓! 여기서도 상태패턴이 빛을 발해요.

"메시지 보내기" 버튼을 눌렀을 때:

  • 연결 중: 메시지를 큐에 저장하고 연결 완료까지 대기
  • 연결 완료: 즉시 메시지 전송
  • 연결 끊김: 재연결 시도하고 메시지는 큐에 저장
  • 연결 실패: 사용자에게 실패 메시지 표시

똑같은 "메시지 보내기" 동작인데 연결 상태에 따라 완전히 다른 처리가 필요하죠.

GOF의 디자인패턴 책에서도 상태패턴의 예시로 TCP 연결을 들었어요. 네트워크 연결 관리는 상태패턴의 대표적인 활용처거든요.

웹소켓에서는 이런 니즈들이 있어요:

  • 중복 연결 방지: 이미 연결 중일 때 또 연결 시도하면 안 됨
  • 메시지 전송 버튼: 상태별로 다른 동작 (전송/대기/재시도)
  • 자동 재연결: 연결이 끊어지면 자동으로 재시도하되, 너무 자주 시도하면 안 됨

이런 복잡한 로직들을 상태패턴으로 각 상태별로 분리하면 훨씬 관리하기 쉬워져요.

결론: 마무리하며

지금까지 상태패턴에 대해 알아봤는데, 어떠셨나요?

여러분도 일상적으로 쓰던 토글부터 복잡한 퍼널까지, 상태패턴은 생각보다 많은 곳에서 활용할 수 있는 패턴이에요. 기획이 자주 바뀌는 프로젝트에서 특히 빛을 발하죠.

핵심 정리

상태패턴의 핵심은 "상태를 단순한 값이 아니라, 동작까지 포함한 객체로 만드는 것"이었어요. 그래서:

  • 분기문 지옥에서 벗어날 수 있고
  • 새로운 상태 추가가 쉬워지고
  • 각 상태의 로직이 캡슐화되어 유지보수가 편해져요

주의사항: 상황에 따라 안티패턴이 될 수 있어요

하지만 디자인패턴은 만능이 아닙니다. 복잡하지 않은 상황에서는 오히려 분기문으로 처리하는 게 좋아요.

예를 들어:

  • 상태가 2-3개뿐이고 앞으로 늘어날 가능성이 낮다면
  • 각 상태의 로직이 매우 간단하다면
  • 팀원들이 상태패턴에 익숙하지 않다면

이런 경우에는 단순한 if/elseswitch문이 더 읽기 쉽고 유지보수하기 좋을 수 있어요.

상태패턴은 복잡성이 충분히 정당화될 때만 사용하는 게 좋습니다.

다음은 어떤 패턴을 다뤄볼까요?

여러분이 만난 디자인패턴은 무엇인가요?

프로젝트에서 사용해본 패턴이나 "이런 상황에서 어떤 패턴을 쓸까?" 같은 고민이 있다면 댓글로 남겨주세요. 다음 글에서 소개해보겠습니다!

옵저버 패턴? 전략 패턴? 팩토리 패턴? 여러분의 관심사를 알려주세요.

읽어주셔서 감사합니다. 🙏

이력서 멘토링 신청받습니다

안녕하세요 최근 사이드로 멘토링을 받고 있습니다.

신청란: 신청란: https://fe-resume.coach?utm_source=velog&utm_medium=blog&utm_campaign=xstate

profile
기부하면 코드 드려요

3개의 댓글

comment-user-thumbnail
2025년 6월 16일

와 글 엄청 야무지네요 사이드플젝에 적용해봐야겠어요 감사합니다!!

답글 달기
comment-user-thumbnail
2025년 6월 25일

옵저버 패턴이 궁금해요~~ 옵저버 패턴으로 zustand 같은 전역 변수를 만들어 보고 싶네요

답글 달기
comment-user-thumbnail
2025년 9월 3일

For anyone looking for Escorts Near Me In Mahipalpur, the experience is sure to be mesmerizing and rewarding. These females are skilled at conjuring up moments of sensuality and companionship. If you're looking for romantic energy, intimate presence, or someone to just make you feel loved, escorts nearby offer it all. Their beauty, grace, and capacity to put you at ease make them the ideal alternative for any gentleman looking for closeness and emotional warmth.

답글 달기