상태는 변할 수 있는 값이 아니다

vx_developer·2026년 9월 11일

개발하다가

목록 보기
20/30
post-thumbnail

프로그래밍을 처음 배울 때 상태는 보통 현재 저장되어 있으며 변경될 수 있는 값이라고 설명한다.

let remainingUses = 3;

remainingUses = 2;

처음에는 쿠폰 사용 횟수가 3이었지만, 쿠폰을 한 번 사용한 뒤 2로 변경되었다.

상태의 기본적인 개념을 이해하기에는 충분한 설명이다.

하지만 실제 서비스를 개발하기 시작하면 값이 변경된다는 이유만으로 모든 값을 상태라고 부르기에는 부족하다.

쿠폰 서비스만 보더라도 서로 다른 종류의 상태가 존재한다.

  • 입력창에 작성 중인 쿠폰 제목
  • 현재 선택된 쿠폰
  • 쿠폰 목록을 불러오는 중인지 나타내는 값
  • 서버에서 받은 쿠폰 목록
  • 쿠폰의 남은 사용 횟수
  • 쿠폰이 활성, 일시 정지, 완료 중 어느 단계인지 나타내는 값
  • 로그인한 사용자
  • 화면에 열려 있는 모달
  • 마지막으로 데이터를 동기화한 시각

이 값들은 모두 변할 수 있지만 같은 생명주기와 책임을 가지지는 않는다.

일부는 화면을 닫으면 사라져도 된다.

일부는 새로고침 후에도 유지되어야 한다.

일부는 다른 사용자와 공유되어야 한다.

일부는 다른 원본 데이터로부터 계산할 수 있다.

일부는 반드시 서버와 데이터베이스가 관리해야 한다.

따라서 상태를 설계할 때는 다음 질문이 필요하다.

  • 이 상태는 현실의 어떤 현재 모습을 표현하는가?
  • 누가 이 상태를 소유하는가?
  • 무엇이 이 상태를 변경할 수 있는가?
  • 얼마나 오래 유지되어야 하는가?
  • 다른 값으로부터 계산할 수 있는가?
  • 서버와 화면 중 어디가 원본인가?
  • 어떤 상태에서 어떤 상태로 이동할 수 있는가?
  • 잘못된 상태 조합을 만들 수 있는가?
  • 동시에 여러 요청이 변경하면 어떻게 되는가?

실제 서비스에서 상태는 단순히 변할 수 있는 값이 아니다. 시간에 따라 달라지는 서비스의 현재 모습을 표현하고, 그 변화의 원인·규칙·소유권을 관리하는 데이터다.


상태는 한 시점의 서비스 모습을 보여준다

현실의 쿠폰에는 현재 모습이 있다.

  • 아직 사용할 수 있다.
  • 남은 사용 횟수가 3회다.
  • 크리스가 수신자다.
  • 2026년 12월 31일에 만료된다.
  • 발행자가 일시 정지했다.
  • 마지막 사용으로 완료되었다.

프로그램에서는 이 모습을 값으로 표현한다.

const coupon = {
  id: "coupon_123",
  title: "Coffee Date",
  recipientId: "user_chris",
  remainingUses: 3,
  status: "active",
  expiresAt:
    new Date("2026-12-31"),
};

이 객체는 쿠폰의 영원히 변하지 않는 정의가 아니다.

특정 시점에 서비스가 알고 있는 쿠폰의 현재 모습이다.

쿠폰을 사용하면 일부 값이 바뀐다.

const redeemedCoupon = {
  ...coupon,
  remainingUses:
    coupon.remainingUses - 1,
};

발행자가 쿠폰을 일시 정지하면 상태가 다시 달라진다.

const pausedCoupon = {
  ...redeemedCoupon,
  status: "paused",
};

상태는 값 하나만 의미하지 않는다.

여러 값이 함께 구성하는 한 시점의 모습일 수 있다.

따라서 상태 변경은 단순히 변수에 새 값을 대입하는 작업이 아니다.

서비스가 알고 있는 현재 사실을 다음 모습으로 전환하는 일이다.


모든 변수는 상태가 아니다

함수 안에서 잠시 사용하는 계산 결과가 있다고 생각해 보자.

function calculateDiscount(
  price: number,
  rate: number
) {
  const discount =
    price * rate;

  return discount;
}

discount는 값을 가지지만 함수 실행이 끝나면 더 이상 유지할 필요가 없다.

현재 화면이나 서비스의 모습을 표현하지도 않는다.

반면 다음 값은 사용자가 어떤 쿠폰을 선택했는지 나타낸다.

const [
  selectedCouponId,
  setSelectedCouponId,
] = useState<string | null>(
  null
);

이 값이 바뀌면 화면의 현재 모습과 가능한 사용자 행동이 달라진다.

문법적으로는 두 값 모두 변수에 저장된다.

하지만 역할은 다르다.

역할
함수의 중간 계산 결과실행 중 잠시 사용하는 값
입력창의 현재 내용화면 상태
선택된 쿠폰 ID사용자 상호작용 상태
서버에서 조회한 쿠폰서버 상태의 화면 사본
쿠폰 사용 횟수서비스가 유지해야 하는 영구 상태

변경 가능성만으로 상태를 판단하면 필요하지 않은 값까지 상태로 저장하게 된다.

상태는 보통 다음 질문과 연결된다.

이 값이 바뀌었을 때 서비스나 화면이 기억해야 할 현재 모습도 달라지는가?


계산할 수 있는 값을 다시 상태로 저장하면 원본이 두 개가 된다

쿠폰에 만료 시각이 저장되어 있다고 생각해 보자.

const expiresAt =
  coupon.expiresAt;

현재 시각과 비교하면 만료 여부를 계산할 수 있다.

const isExpired =
  expiresAt !== null &&
  expiresAt <= now;

그런데 isExpired까지 별도의 상태로 저장할 수 있다.

const [
  isExpired,
  setIsExpired,
] = useState(false);

이제 두 값이 같은 사실을 표현한다.

  • expiresAt
  • isExpired

두 값을 항상 함께 변경하지 않으면 서로 모순될 수 있다.

expiresAt =
  new Date("2026-01-01");

// isExpired는 여전히 false다.

가능하면 원본 상태만 저장하고 계산 가능한 값은 필요할 때 만든다.

const isExpired =
  coupon.expiresAt !== null &&
  coupon.expiresAt <= now;

쿠폰 목록에서도 마찬가지다.

const activeCoupons =
  coupons.filter(
    (coupon) =>
      coupon.status === "active"
  );

couponsactiveCoupons를 모두 독립적인 상태로 저장하면 쿠폰이 변경될 때 두 목록을 함께 갱신해야 한다.

const [
  coupons,
  setCoupons,
] = useState<Coupon[]>([]);
const activeCoupons =
  coupons.filter(
    (coupon) =>
      coupon.status === "active"
  );

이 구조에서는 coupons가 원본 상태이고 activeCoupons는 계산된 결과다.

상태를 줄인다는 것은 정보를 잃는 일이 아니다. 같은 사실을 표현하는 원본을 하나로 유지하는 일이다.


상태의 원본이 어디인지 먼저 결정해야 한다

쿠폰의 남은 사용 횟수가 화면과 서버에 모두 존재한다고 생각해 보자.

const [
  remainingUses,
  setRemainingUses,
] = useState(
  coupon.remainingUses
);

사용자가 쿠폰 사용 버튼을 누르면 화면에서 먼저 값을 감소시킬 수 있다.

setRemainingUses(
  remainingUses - 1
);

하지만 실제 쿠폰 사용은 서버에서 실패할 수 있다.

  • 이미 다른 요청이 마지막 횟수를 사용했다.
  • 쿠폰이 방금 만료되었다.
  • 발행자가 쿠폰을 일시 정지했다.
  • 네트워크 요청이 실패했다.
  • 현재 사용자에게 사용 권한이 없다.

화면의 값이 바뀌었다고 서비스의 실제 상태까지 바뀐 것은 아니다.

영구적인 쿠폰 상태의 원본은 서버와 데이터베이스에 있다.

const updatedCoupon =
  await redeemCoupon(
    coupon.id
  );

setCoupon(updatedCoupon);

서버가 성공한 결과를 반환한 뒤 화면 상태를 갱신하면 원본과 사본의 관계가 분명해진다.

빠른 반응을 위해 화면을 먼저 변경하는 낙관적 업데이트를 사용할 수도 있다.

const previousCoupon =
  coupon;

setCoupon({
  ...coupon,
  remainingUses:
    coupon.remainingUses - 1,
});

try {
  const savedCoupon =
    await redeemCoupon(
      coupon.id
    );

  setCoupon(savedCoupon);
} catch {
  setCoupon(previousCoupon);
}

이 경우에도 서버가 원본이라는 사실은 바뀌지 않는다.

화면은 성공을 예상해 임시 상태를 보여주고, 실패하면 이전 상태로 복구한다.


같은 값도 책임에 따라 서로 다른 상태가 된다

쿠폰 제목 "Coffee Date"는 여러 위치에 존재할 수 있다.

const [
  draftTitle,
  setDraftTitle,
] = useState(
  "Coffee Date"
);

이 값은 사용자가 편집 중인 임시 화면 상태다.

const savedCoupon =
  await couponRepository.findById(
    couponId
  );

데이터베이스에서 조회한 제목은 저장된 서비스 상태다.

두 값이 같아 보여도 책임은 다르다.

사용자가 입력창을 수정했다고 저장된 쿠폰이 즉시 변경되는 것은 아니다.

setDraftTitle(
  "Dinner Date"
);

이 시점에는 화면의 초안만 바뀐다.

저장 요청이 성공해야 영구 상태가 바뀐다.

const updatedCoupon =
  await updateCouponTitle({
    couponId,
    title: draftTitle,
  });

따라서 상태 이름은 값의 내용뿐 아니라 역할도 표현해야 한다.

draftTitle
savedTitle
pendingTitle

모두 문자열이지만 서로 다른 단계의 사실을 나타낸다.

상태를 설계할 때는 같은 값을 여러 위치에 두지 말아야 한다는 단순한 규칙보다 다음 질문이 중요하다.

각 값은 어느 단계의 현실을 표현하며, 누가 그 상태를 확정할 책임이 있는가?


상태의 위치는 생명주기와 공유 범위가 결정한다

상태를 어디에 저장할지는 특정 라이브러리의 선호로 결정되지 않는다.

값이 얼마나 오래 존재해야 하고 누구와 공유되어야 하는지에 따라 달라진다.

상태의 성격적절한 위치
함수 실행 중에만 필요지역 변수
한 컴포넌트에서만 사용로컬 UI 상태
가까운 여러 컴포넌트가 공유공통 부모 또는 제한된 Context
URL로 공유·복원해야 함경로 또는 쿼리 문자열
새로고침 후 기기에 유지브라우저 저장소
서버에서 조회한 데이터서버 상태 관리 또는 요청 캐시
사용자별 영구 데이터데이터베이스
여러 서버 인스턴스가 공유데이터베이스 또는 공유 저장소
환경마다 다른 설정환경변수 또는 설정 시스템

쿠폰 검색어는 화면에만 필요한 상태일 수 있다.

const [
  searchQuery,
  setSearchQuery,
] = useState("");

하지만 검색 결과 페이지를 링크로 공유해야 한다면 URL 상태가 더 적절할 수 있다.

/coupons?query=coffee&status=active

로그인한 사용자에게 영구적으로 귀속되는 쿠폰은 브라우저 메모리에만 저장해서는 안 된다.

await couponRepository.save(
  coupon
);

상태 관리 도구를 선택하기 전에 상태의 생명주기와 소유자를 먼저 결정해야 한다.


Boolean이 늘어나면 존재할 수 없는 상태가 만들어진다

쿠폰을 불러오는 화면을 여러 Boolean으로 표현할 수 있다.

const [
  isLoading,
  setIsLoading,
] = useState(false);

const [
  isSuccess,
  setIsSuccess,
] = useState(false);

const [
  isError,
  setIsError,
] = useState(false);

이 구조에서는 다음과 같은 조합도 문법적으로 가능하다.

isLoading = true;
isSuccess = true;
isError = true;

실제로는 로딩, 성공, 실패가 동시에 발생한 상태를 원하지 않을 수 있다.

가능한 상태를 하나의 타입으로 표현할 수 있다.

type CouponRequestState =
  | {
      status: "idle";
    }
  | {
      status: "loading";
    }
  | {
      status: "success";
      coupon: Coupon;
    }
  | {
      status: "error";
      message: string;
    };

이제 한 시점에는 하나의 상태만 존재한다.

const [
  requestState,
  setRequestState,
] = useState<CouponRequestState>({
  status: "idle",
});

성공 상태에서만 쿠폰 데이터가 존재한다.

if (
  requestState.status ===
  "success"
) {
  console.log(
    requestState.coupon
  );
}

상태 타입을 설계한다는 것은 값을 분류하는 일에 그치지 않는다.

서비스와 화면에서 존재할 수 있는 경우와 존재해서는 안 되는 조합을 정의하는 일이다.


상태 변경은 대입보다 전이로 바라봐야 한다

쿠폰 상태를 직접 변경할 수 있다고 생각해 보자.

coupon.status = "completed";

이 코드는 결과만 보여준다.

왜 완료되었는지, 이전 상태가 무엇이었는지, 해당 변경이 허용되는지는 알기 어렵다.

서비스의 행동으로 상태를 변경할 수 있다.

function redeemCoupon(
  coupon: Coupon,
  now: Date
): Coupon {
  if (
    coupon.status !== "active"
  ) {
    throw new Error(
      "활성 쿠폰만 사용할 수 있다."
    );
  }

  if (
    coupon.expiresAt !== null &&
    coupon.expiresAt <= now
  ) {
    throw new Error(
      "만료된 쿠폰이다."
    );
  }

  const remainingUses =
    coupon.remainingUses - 1;

  return {
    ...coupon,
    remainingUses,
    status:
      remainingUses === 0
        ? "completed"
        : "active",
  };
}

이 함수는 단순히 속성을 수정하지 않는다.

  • 현재 상태가 전이를 허용하는지 확인한다.
  • 사용 횟수를 감소시킨다.
  • 남은 횟수가 없으면 완료 상태로 전환한다.

쿠폰의 상태 변화는 다음과 같이 표현할 수 있다.

stateDiagram-v2
    [*] --> Active
    Active --> Paused: 발행자가 일시 정지
    Paused --> Active: 발행자가 재개
    Active --> Completed: 마지막 횟수 사용
    Active --> Expired: 만료 시각 도달
    Paused --> Expired: 만료 시각 도달

상태 전이를 명시하면 허용되지 않는 변화도 찾기 쉬워진다.

예를 들어 완료된 쿠폰을 다시 활성 상태로 되돌리는 전이는 정책에 없다.

좋은 상태 설계는 현재 값을 보여주는 것뿐 아니라 어떤 원인으로 어떤 다음 상태가 허용되는지 설명한다.


상태 이름은 기술적 변경보다 서비스의 의미를 표현해야 한다

다음 함수는 쿠폰 상태를 원하는 값으로 변경한다.

function setCouponStatus(
  coupon: Coupon,
  status: CouponStatus
) {
  return {
    ...coupon,
    status,
  };
}

호출자는 모든 상태를 자유롭게 지정할 수 있다.

setCouponStatus(
  coupon,
  "completed"
);

하지만 이 코드에서는 왜 쿠폰이 완료되었는지 알기 어렵다.

서비스 행동을 이름으로 표현하면 상태 변화의 이유가 드러난다.

redeemCoupon(coupon, now);
pauseCoupon(coupon, issuerId);
resumeCoupon(coupon, issuerId);

setStatus("paused")는 속성 변경을 설명한다.

pauseCoupon은 서비스에서 일어난 행동을 설명한다.

상태는 결과이고 행동은 변화의 원인이다.

상태 변경 함수를 설계할 때는 “어떤 값을 설정할 것인가?”보다 “서비스에서 무슨 일이 일어났는가?”를 먼저 표현해야 한다.


이전 상태를 기준으로 변경해야 할 때는 현재 값을 다시 읽어야 한다

React 화면에서 쿠폰 수량을 연속으로 변경한다고 생각해 보자.

setQuantity(quantity + 1);
setQuantity(quantity + 1);

두 호출이 같은 quantity 값을 읽으면 예상보다 한 번만 증가할 수 있다.

이전 상태를 기반으로 다음 상태를 계산한다는 사실을 명시할 수 있다.

setQuantity(
  (currentQuantity) =>
    currentQuantity + 1
);

setQuantity(
  (currentQuantity) =>
    currentQuantity + 1
);

각 변경은 가장 최근 상태를 기준으로 계산된다.

서버에서도 같은 문제가 더 큰 범위로 나타난다.

const coupon =
  await findCoupon(couponId);

coupon.remainingUses -= 1;

await saveCoupon(coupon);

여러 요청이 같은 쿠폰을 읽으면 오래된 상태를 기준으로 변경할 수 있다.

화면의 함수형 업데이트와 데이터베이스의 트랜잭션은 같은 기술은 아니다.

그러나 둘 다 다음 문제를 다룬다.

다음 상태가 이전 상태에 의존할 때 어떤 이전 상태를 기준으로 계산할 것인가?


서버 상태를 복사하면 동기화 책임도 생긴다

API에서 받은 쿠폰 목록을 로컬 상태에 복사할 수 있다.

const {
  data: coupons,
} = useQuery({
  queryKey: ["coupons"],
  queryFn: fetchCoupons,
});

const [
  localCoupons,
  setLocalCoupons,
] = useState(coupons);

이제 같은 목록을 나타내는 상태가 두 곳에 존재한다.

  • 요청 캐시의 coupons
  • 컴포넌트의 localCoupons

서버에서 새 데이터를 받아도 localCoupons가 자동으로 같은 값이 된다고 보장할 수 없다.

단순히 목록을 보여주기만 한다면 서버 상태를 직접 사용할 수 있다.

const visibleCoupons =
  coupons?.filter(
    (coupon) =>
      coupon.status === "active"
  ) ?? [];

별도의 로컬 사본이 필요한 경우도 있다.

예를 들어 사용자가 여러 항목을 편집한 뒤 한 번에 저장하는 초안 화면이다.

이때는 두 상태의 의미를 이름으로 구분해야 한다.

const serverCoupon =
  query.data;

const [
  couponDraft,
  setCouponDraft,
] = useState(
  createDraft(serverCoupon)
);

그리고 다음 상황을 설계해야 한다.

  • 서버 데이터가 갱신되면 초안을 덮어쓸 것인가?
  • 사용자의 수정 내용과 서버 변경이 충돌하면 어떻게 할 것인가?
  • 취소 버튼은 어느 상태로 복원할 것인가?
  • 저장 성공 후 어떤 값을 새 원본으로 사용할 것인가?

상태를 복사하는 순간 동기화 규칙도 함께 만들어야 한다.


상태가 저장되었다고 변화의 이유까지 남는 것은 아니다

데이터베이스에 현재 쿠폰 상태만 저장할 수 있다.

{
  "id": "coupon_123",
  "status": "paused",
  "remainingUses": 2
}

현재 모습을 확인하기에는 충분하다.

하지만 다음 질문에는 답하기 어렵다.

  • 누가 쿠폰을 일시 정지했는가?
  • 언제 변경되었는가?
  • 어떤 이유로 변경되었는가?
  • 이전에는 어떤 상태였는가?
  • 사용 횟수가 언제 감소했는가?

운영과 감사에 필요한 경우 상태 변경 기록을 남길 수 있다.

const history = {
  couponId: coupon.id,
  action: "paused",
  actorId: currentUser.id,
  occurredAt: now,
  reason: input.reason,
};

현재 상태와 변경 이력은 서로 다른 목적을 가진다.

  • 현재 상태는 지금 무엇이 가능한지 빠르게 판단한다.
  • 변경 이력은 어떻게 현재 상태에 도달했는지 설명한다.

모든 상태 변경에 완전한 이벤트 기록이 필요한 것은 아니다.

그러나 결제, 권한, 쿠폰 사용처럼 변화의 이유를 추적해야 하는 기능에서는 현재 값만 저장하는 것으로 충분한지 확인해야 한다.


클라이언트가 보낸 상태를 그대로 믿어서는 안 된다

쿠폰 수정 요청이 다음과 같다고 생각해 보자.

{
  "couponId": "coupon_123",
  "status": "completed",
  "remainingUses": 100
}

서버가 요청 객체를 그대로 저장하면 사용자가 허용되지 않은 상태를 만들 수 있다.

await couponRepository.update(
  request.body
);

클라이언트는 수행하려는 행동을 요청하고, 서버가 다음 상태를 결정하는 편이 안전하다.

await redeemCoupon({
  couponId:
    request.body.couponId,
  userId:
    request.currentUser.id,
});

서버는 현재 상태와 규칙을 확인한다.

const nextCoupon =
  coupon.redeem({
    userId,
    now,
  });

사용자가 “남은 횟수를 2로 바꿔 달라”고 요청하는 것과 “쿠폰을 한 번 사용하겠다”고 요청하는 것은 다르다.

전자는 결과 상태를 외부가 결정한다.

후자는 서비스 행동을 요청하고 서버가 결과를 계산한다.


상태를 설계하기 전에 물어봐야 할 질문

의미와 원본

  1. 이 상태는 현실의 어떤 현재 모습을 표현하는가?
  2. 이 값의 원본은 화면, 서버, 데이터베이스 중 어디에 있는가?
  3. 같은 사실을 표현하는 상태가 여러 곳에 저장되어 있지는 않은가?
  4. 다른 상태로부터 계산할 수 있는 값인가?
  5. 임시 값과 확정된 값을 이름으로 구분하고 있는가?

소유권과 생명주기

  1. 누가 이 상태를 읽고 변경해야 하는가?
  2. 한 컴포넌트 안에서만 필요한가?
  3. 새로고침 후에도 유지되어야 하는가?
  4. 다른 사용자나 서버 인스턴스와 공유되어야 하는가?
  5. 상태의 생명주기보다 지나치게 넓은 저장소를 사용하고 있지는 않은가?

전이와 규칙

  1. 가능한 상태에는 무엇이 있는가?
  2. 존재해서는 안 되는 상태 조합을 만들 수 있는가?
  3. 어떤 상태에서 어떤 상태로 이동할 수 있는가?
  4. 상태 변경을 일으키는 서비스 행동은 무엇인가?
  5. 직접 속성을 수정해 규칙을 우회할 수 있는가?

동기화와 동시성

  1. 서버 상태의 로컬 사본을 만들고 있는가?
  2. 원본이 바뀌면 사본을 어떻게 갱신하는가?
  3. 낙관적 업데이트가 실패하면 어떻게 복구하는가?
  4. 여러 요청이 같은 이전 상태를 기준으로 변경할 수 있는가?
  5. 충돌을 막기 위해 트랜잭션이나 버전 검사가 필요한가?

저장과 추적

  1. 현재 상태는 어디에 영구 저장되는가?
  2. 메모리 변경과 데이터베이스 변경을 구분하고 있는가?
  3. 변경한 사람과 이유를 기록해야 하는가?
  4. 현재 상태만으로 운영 문제를 조사할 수 있는가?
  5. 캐시나 브라우저 저장소의 오래된 상태를 어떻게 갱신하는가?

흔히 하는 실수

변경 가능한 값은 모두 상태로 저장한다

const [
  discountedPrice,
  setDiscountedPrice,
] = useState(0);

pricediscountRate로 계산할 수 있다면 별도 상태가 필요하지 않을 수 있다.

const discountedPrice =
  price -
  price * discountRate;

계산 가능한 값은 원본 상태에서 파생한다.


같은 사실을 여러 상태로 저장한다

const [
  coupons,
  setCoupons,
] = useState<Coupon[]>([]);

const [
  activeCoupons,
  setActiveCoupons,
] = useState<Coupon[]>([]);

두 목록을 항상 함께 갱신해야 한다.

const activeCoupons =
  coupons.filter(
    (coupon) =>
      coupon.status === "active"
  );

하나를 원본으로 두고 나머지는 계산한다.


여러 Boolean으로 하나의 상태를 표현한다

isLoading
isSuccess
isError

서로 모순되는 조합이 만들어질 수 있다.

type RequestState =
  | "idle"
  | "loading"
  | "success"
  | "error";

가능한 상태를 명시적으로 제한한다.


상태 속성을 직접 변경한다

coupon.status = "completed";

변경의 이유와 허용 조건이 보이지 않는다.

coupon.redeem(context);

서비스 행동을 통해 다음 상태를 만든다.


서버 상태를 로컬 상태에 불필요하게 복사한다

const [
  localCoupons,
  setLocalCoupons,
] = useState(
  query.data
);

원본과 사본의 동기화 문제가 생긴다.

편집 초안처럼 다른 의미가 필요한 경우에만 복사하고 그 차이를 명확히 표현한다.


메모리 상태가 바뀌면 저장도 끝났다고 생각한다

coupon.remainingUses -= 1;

이 코드는 현재 프로세스의 객체만 변경한다.

await couponRepository.save(
  coupon
);

영구 상태 변경은 저장 성공까지 확인해야 한다.


클라이언트가 요청한 다음 상태를 그대로 저장한다

coupon.status =
  request.body.status;

외부 입력이 상태 전이 규칙을 우회할 수 있다.

coupon.pause(
  request.currentUser.id
);

클라이언트는 행동을 요청하고 서버가 다음 상태를 결정하도록 한다.


핵심 정리

상태는 현재 저장되어 있으며 변경될 수 있는 값으로 설명할 수 있다.

let remainingUses = 3;

remainingUses = 2;

그러나 실제 서비스에서 상태는 단순히 바뀌는 변수가 아니다.

상태는 특정 시점에 서비스가 알고 있는 현재 모습을 표현한다.

const coupon = {
  status: "active",
  remainingUses: 2,
  recipientId: "user_chris",
};

좋은 상태 설계는 다음 내용을 분명하게 만든다.

  • 상태가 현실의 무엇을 표현하는가
  • 어느 값이 원본인가
  • 어떤 값은 계산해서 만들 수 있는가
  • 누가 상태를 소유하고 변경하는가
  • 얼마나 오래 유지되어야 하는가
  • 어디에 저장해야 하는가
  • 가능한 상태와 불가능한 조합은 무엇인가
  • 어떤 행동이 상태 전이를 일으키는가
  • 서버와 화면 상태를 어떻게 동기화하는가
  • 동시에 변경될 때 일관성을 어떻게 지키는가
  • 변화의 이유와 이력을 남겨야 하는가

상태의 흐름은 다음과 같이 볼 수 있다.

flowchart LR
    A[현재 상태]
    --> B[사용자 또는 시스템 행동]
    --> C[규칙 확인]
    --> D[다음 상태 계산]
    --> E[영구 저장]
    --> F[화면과 다른 시스템에 반영]

상태는 결과이고 행동은 변화의 원인이다.

따라서 단순히 속성에 값을 대입하기보다 어떤 행동이 어떤 규칙을 통해 다음 상태를 만드는지 표현해야 한다.

상태를 설계한다는 것은 결국 다음 질문에 답하는 일이다.

서비스가 지금 어떤 모습인지 무엇으로 표현하며, 누가 어떤 이유와 규칙으로 그 모습을 다음 상태로 바꿀 수 있는가?

상태는 변할 수 있는 값이 아니다.

시간에 따라 달라지는 서비스의 현재 모습을 표현하고, 그 변화가 올바른 경로를 따르도록 관리하는 방법이다.

profile
Vision eXperience Developer

0개의 댓글