비동기는 기다리지 않고 다음 코드를 실행하는 것이 아니다

vx_developer·2일 전

개발하다가

목록 보기
28/30
post-thumbnail

프로그래밍을 처음 배울 때 비동기는 보통 “작업이 끝날 때까지 기다리지 않고 다음 코드를 실행하는 방식”이라고 배운다.

console.log("예약 가능 시간 조회 시작");

fetch("/api/reservation-slots");

console.log("다음 코드 실행");

fetch의 응답이 도착하기 전에 다음 로그가 출력된다는 사실을 이해하기에는 충분한 설명이다.

하지만 실제 예약 서비스를 개발하면 단순히 기다리는지 기다리지 않는지만으로는 부족하다.

  • 예약 가능 시간은 언제 화면에 표시해야 하는가?
  • 여러 요청 중 어떤 응답을 최신 결과로 사용해야 하는가?
  • 로딩 중 사용자가 날짜를 다시 변경하면 어떻게 해야 하는가?
  • 요청이 실패하거나 너무 오래 걸리면 무엇을 보여줘야 하는가?
  • 예약 버튼을 여러 번 누르면 같은 예약이 중복 생성되지 않는가?
  • 서로 의존하는 작업과 독립적인 작업은 어떤 순서로 실행해야 하는가?

비동기 작업을 다룬다는 것은 코드를 빨리 실행하는 일이 아니다. 시간이 걸리는 작업의 시작, 대기, 완료, 실패, 취소와 실행 순서를 서비스 흐름에 맞게 설계하는 일이다.

실제 서비스에서 비동기는 시간이 걸리는 작업의 순서와 실패를 관리하는 방법이다.


비동기 작업에는 결과가 도착하지 않은 시간이 존재한다

크리스가 예약 날짜를 선택하면 서버에서 가능한 시간대를 조회한다고 생각해 보자.

async function loadReservationSlots(date: string) {
  const response = await fetch(
    `/api/reservation-slots?date=${date}`
  );

  return response.json();
}

await를 사용하면 코드는 위에서 아래로 읽힌다. 그러나 네트워크 요청 자체가 즉시 끝나는 것은 아니다.

요청을 보낸 뒤 결과가 도착하기 전까지 서비스에는 분명한 중간 상태가 존재한다.

type ReservationSlotState =
  | { status: "idle" }
  | { status: "loading" }
  | {
      status: "success";
      slots: ReservationSlot[];
    }
  | {
      status: "error";
      message: string;
    };

이 상태는 단순한 구현 세부사항이 아니다.

  • idle에서는 날짜 선택을 안내한다.
  • loading에서는 조회 중임을 보여준다.
  • success에서는 예약 가능한 시간대를 표시한다.
  • error에서는 실패 원인과 다시 시도할 방법을 제공한다.

비동기 작업을 시작하는 순간부터 화면은 완료 전 상태를 표현할 책임을 갖는다.

로딩 상태 없이 이전 결과를 그대로 보여주면 사용자는 새로 선택한 날짜의 예약 시간이라고 오해할 수 있다. 오류 상태 없이 빈 배열만 반환하면 예약이 모두 찬 것인지 조회에 실패한 것인지 구분할 수 없다.

비동기 작업의 결과만 설계해서는 충분하지 않다. 결과가 아직 없는 시간도 서비스 상태로 설계해야 한다.


await는 전체 프로그램을 멈추지 않는다

다음 코드는 예약 시간 조회가 끝날 때까지 기다리는 것처럼 보인다.

async function showSlots(date: string) {
  const slots = await loadReservationSlots(date);

  renderReservationSlots(slots);
}

여기서 멈추는 것은 showSlots 함수 내부의 다음 단계다. 브라우저 전체와 다른 요청까지 멈추는 것은 아니다.

조회가 진행되는 동안에도 사용자는 버튼을 누르거나 다른 날짜를 선택할 수 있다. 다른 이벤트 핸들러도 실행될 수 있다.

showSlots("2026-09-20");

console.log("사용자는 다른 UI를 계속 사용할 수 있다.");

따라서 await를 사용했다는 이유만으로 사용자 행동의 순서까지 자동으로 보장되지는 않는다.

“코드가 기다린다”는 표현보다 다음과 같이 이해하는 편이 정확하다.

await는 해당 비동기 결과가 필요한 함수의 다음 단계를 보류하고, 결과가 준비되면 그 지점부터 실행을 이어간다.

이 차이를 이해해야 로딩 중 버튼 처리, 중복 요청과 오래된 응답 문제를 설계할 수 있다.


순차 실행은 작업 사이에 의존성이 있을 때 필요하다

크리스가 예약을 완료한 뒤 예약 상세 정보를 가져와 확인 화면을 보여준다고 생각해 보자.

async function completeReservation(
  input: CreateReservationInput
) {
  const reservation =
    await createReservation(input);

  const details =
    await getReservationDetails(
      reservation.id
    );

  return details;
}

두 번째 요청에는 첫 번째 작업에서 생성된 reservation.id가 필요하다.

따라서 두 작업은 순서대로 실행되어야 한다.

예약 생성
  ↓ reservation.id 필요
예약 상세 조회
  ↓
확인 화면 표시

이런 경우 순차 실행은 불필요한 지연이 아니라 데이터 의존성을 표현한다.

반대로 의존성이 없는 작업까지 무조건 차례로 기다리면 전체 대기 시간이 길어진다.

const profile =
  await getCurrentUserProfile();

const policies =
  await getReservationPolicies();

사용자 프로필과 예약 정책 조회가 서로의 결과를 필요로 하지 않는다면 두 요청을 동시에 시작할 수 있다.

const [profile, policies] =
  await Promise.all([
    getCurrentUserProfile(),
    getReservationPolicies(),
  ]);

순차 실행과 동시 시작 중 무엇이 더 좋은지는 문법으로 결정되지 않는다.

먼저 다음 질문에 답해야 한다.

  • 두 번째 작업에 첫 번째 결과가 필요한가?
  • 한 작업이 실패하면 다른 작업도 중단되어야 하는가?
  • 동시에 요청했을 때 서버나 외부 시스템에 과도한 부하를 주지 않는가?
  • 결과를 모두 받아야 다음 화면으로 이동할 수 있는가?

비동기 설계에서 중요한 것은 무조건 병렬화하는 일이 아니라 작업 사이의 의존성을 정확하게 표현하는 일이다.


완료 순서는 시작 순서와 같지 않을 수 있다

크리스가 9월 20일을 선택한 직후 9월 21일로 날짜를 변경했다고 생각해 보자.

loadReservationSlots("2026-09-20");
loadReservationSlots("2026-09-21");

두 번째 요청이 나중에 시작되었다고 해서 반드시 나중에 끝나는 것은 아니다.

09-20 요청 시작
09-21 요청 시작
09-21 응답 완료
09-20 응답 완료

응답이 도착할 때마다 화면을 갱신하면 마지막에는 이전 날짜인 9월 20일의 시간대가 표시될 수 있다.

async function selectDate(date: string) {
  const slots =
    await loadReservationSlots(date);

  setReservationSlots(slots);
}

코드는 단순하지만 오래된 응답이 최신 화면을 덮어쓸 수 있다.

현재 선택값을 확인하도록 개선할 수 있다.

let selectedDate = "";

async function selectDate(date: string) {
  selectedDate = date;

  const slots =
    await loadReservationSlots(date);

  if (date !== selectedDate) {
    return;
  }

  setReservationSlots(slots);
}

이제 응답이 도착했을 때 요청에 사용한 날짜와 현재 선택된 날짜가 같은지 확인한다.

현재 선택값이 바뀌었다면 해당 응답은 정상적으로 완료되었더라도 더 이상 화면에 사용할 수 없는 오래된 결과다.

비동기 작업의 성공 여부와 현재 서비스에 적용할 수 있는지는 서로 다른 문제다.


필요 없어진 요청은 취소할 수 있어야 한다

오래된 응답을 무시하는 것만으로 화면의 정확성은 지킬 수 있다. 하지만 필요 없어진 네트워크 요청은 계속 실행된다.

날짜가 바뀔 때 이전 조회를 취소할 수 있다.

let slotRequestController:
  AbortController | null = null;

async function selectDate(date: string) {
  slotRequestController?.abort();

  slotRequestController =
    new AbortController();

  try {
    const response = await fetch(
      `/api/reservation-slots?date=${date}`,
      {
        signal:
          slotRequestController.signal,
      }
    );

    const slots = await response.json();

    setReservationSlots(slots);
  } catch (error) {
    if (
      error instanceof DOMException &&
      error.name === "AbortError"
    ) {
      return;
    }

    showSlotLoadError();
  }
}

새로운 날짜가 선택되면 기존 AbortControllerabort를 호출한다. 취소로 발생한 오류는 일반적인 장애와 구분한다.

사용자가 직접 필요 없게 만든 요청까지 “예약 시간 조회 실패”로 표시하면 잘못된 경험이 된다.

취소는 실패가 아니다. 더 이상 결과가 필요하지 않다는 서비스의 판단이다.

다만 클라이언트가 요청을 취소했다고 해서 서버의 작업까지 항상 되돌아가는 것은 아니다. 서버가 이미 요청을 처리했을 수 있기 때문이다.

특히 예약 생성과 같은 변경 요청은 화면에서 취소되었다는 이유만으로 실행되지 않았다고 가정해서는 안 된다. 서버의 최신 예약 상태를 다시 확인해야 한다.


오래 걸리는 요청에는 시간 제한이 필요하다

네트워크 요청은 성공하거나 즉시 실패한다고 보장되지 않는다. 연결 상태와 외부 시스템 문제에 따라 오랫동안 완료되지 않을 수 있다.

async function fetchWithTimeout(
  url: string,
  timeoutMs: number
) {
  const controller =
    new AbortController();

  const timeoutId = setTimeout(() => {
    controller.abort();
  }, timeoutMs);

  try {
    return await fetch(url, {
      signal: controller.signal,
    });
  } finally {
    clearTimeout(timeoutId);
  }
}

시간 제한은 단순히 요청을 빨리 포기하기 위한 기능이 아니다.

사용자가 어느 정도 기다린 뒤 다음 행동을 선택할 수 있도록 작업의 경계를 정하는 것이다.

try {
  const response =
    await fetchWithTimeout(
      "/api/reservation-slots",
      5000
    );

  // 응답 처리
} catch (error) {
  showMessage(
    "예약 가능 시간을 불러오지 못했다. 다시 시도할 수 있다."
  );
}

시간이 초과되었을 때는 다음 사항을 구분해야 한다.

  • 조회 요청이라 안전하게 다시 시도할 수 있는가?
  • 예약 생성 요청이 서버에서 이미 처리되었을 가능성이 있는가?
  • 요청 결과를 별도 조회할 수 있는가?
  • 사용자가 같은 작업을 반복해도 중복 예약이 생기지 않는가?

시간 초과는 작업이 실행되지 않았다는 증거가 아니다. 정해진 시간 안에 클라이언트가 결과를 확인하지 못했다는 뜻이다.


비동기 실패는 한 종류가 아니다

다음 코드는 모든 실패를 같은 메시지로 처리한다.

try {
  await createReservation(input);
} catch {
  showMessage("오류가 발생했다.");
}

이 구현에서는 사용자가 무엇을 해야 하는지 알 수 없다.

예약 요청에는 서로 다른 실패가 존재할 수 있다.

  • 네트워크 연결이 끊겼다.
  • 서버가 요청 본문을 해석하지 못했다.
  • 입력한 날짜나 인원수가 유효하지 않다.
  • 선택한 시간대가 다른 사용자에게 먼저 예약되었다.
  • 서버가 일시적으로 요청을 처리하지 못했다.
  • 요청은 처리되었지만 응답이 클라이언트에 도착하지 않았다.

실패의 원인과 복구 방법이 다르다면 코드에서도 구분해야 한다.

type ReservationError =
  | {
      type: "validation";
      fields: Record<string, string[]>;
    }
  | {
      type: "slot-unavailable";
      message: string;
    }
  | {
      type: "network";
      message: string;
    }
  | {
      type: "unknown";
      requestId?: string;
    };

클라이언트는 오류 종류에 따라 다른 행동을 제공할 수 있다.

function handleReservationError(
  error: ReservationError
) {
  switch (error.type) {
    case "validation":
      showFieldErrors(error.fields);
      break;

    case "slot-unavailable":
      refreshReservationSlots();
      break;

    case "network":
      showRetryButton();
      break;

    case "unknown":
      showSupportMessage(
        error.requestId
      );
      break;
  }
}

입력 오류에는 필드 수정을 안내한다. 이미 예약된 시간대라면 최신 시간대를 다시 조회한다. 네트워크 오류에는 재시도 방법을 제공한다.

비동기 오류 처리는 catch를 작성하는 데서 끝나지 않는다. 실패를 분류하고 다음 행동을 연결해야 한다.


외부에서 도착한 응답은 검증 전까지 신뢰할 수 없다

서버가 다음과 같은 예약 가능 시간 목록을 반환한다고 가정해 보자.

{
  "slots": [
    {
      "id": "slot_42",
      "startTime": "2026-09-20T10:00:00+10:00",
      "available": true
    }
  ]
}

TypeScript 타입을 선언했다고 해서 실제 응답이 그 구조를 따른다고 보장되지는 않는다.

type ReservationSlot = {
  id: string;
  startTime: string;
  available: boolean;
};

const data =
  (await response.json()) as {
    slots: ReservationSlot[];
  };

as는 런타임 데이터를 검증하지 않는다. 개발자가 컴파일러에 타입을 믿으라고 지시할 뿐이다.

API 응답, URL 파라미터와 사용자 입력처럼 서비스 외부에서 들어오는 값은 검증 전까지 신뢰할 수 없다.

const reservationSlotSchema = z.object({
  id: z.string().min(1),
  startTime: z.string().datetime({
    offset: true,
  }),
  available: z.boolean(),
});

const responseSchema = z.object({
  slots: z.array(
    reservationSlotSchema
  ),
});

const data = responseSchema.parse(
  await response.json()
);

이제 응답 구조가 계약과 다르면 정상적인 예약 목록처럼 사용하지 않고 명시적인 검증 실패로 처리할 수 있다.

비동기 작업이 성공적으로 완료되었다는 사실은 도착한 데이터가 올바르다는 뜻이 아니다.

네트워크 성공, HTTP 처리 성공과 데이터 검증 성공은 각각 별도의 단계다.


예약 버튼을 비활성화하는 것만으로 중복 실행을 막을 수는 없다

예약 제출 중 버튼을 비활성화하면 사용자의 반복 클릭을 줄일 수 있다.

<button
  disabled={status === "submitting"}
  onClick={submitReservation}
>
  예약하기
</button>

필요한 사용자 경험이지만 중복 예약을 완전히 방지하지는 못한다.

  • 버튼이 비활성화되기 전에 빠르게 두 번 클릭할 수 있다.
  • 사용자가 여러 브라우저 탭을 열 수 있다.
  • 네트워크 오류 후 같은 요청을 다시 보낼 수 있다.
  • 모바일 앱과 웹에서 같은 예약을 동시에 요청할 수 있다.

클라이언트의 로딩 상태는 사용자 인터페이스를 관리한다. 예약의 최종 일관성은 서버가 보장해야 한다.

await fetch("/api/reservations", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Idempotency-Key":
      reservationAttemptId,
  },
  body: JSON.stringify(input),
});

같은 예약 시도를 재전송할 때 동일한 멱등성 키를 사용하면 서버는 이미 처리한 요청인지 판단할 수 있다.

또한 서버는 예약을 확정하기 직전에 시간대의 최신 상태를 확인해야 한다.

화면에 표시된 available: true는 조회 시점의 복사본이다. 예약 가능 여부의 Source of Truth는 서버가 관리하는 최신 예약 상태다.

비동기 작업 사이에는 시간이 흐른다. 그동안 데이터가 바뀔 수 있다는 사실을 설계에 포함해야 한다.


독립적인 후속 작업은 핵심 결과와 분리할 수 있다

예약이 생성된 뒤 확인 이메일과 분석 이벤트를 처리한다고 생각해 보자.

async function createReservationFlow(
  input: CreateReservationInput
) {
  const reservation =
    await reservationRepository.create(
      input
    );

  await sendConfirmationEmail(
    reservation
  );

  await trackReservationCreated(
    reservation
  );

  return reservation;
}

이 구조에서는 이메일 서비스가 느리거나 분석 시스템이 실패하면 사용자가 예약 성공 응답을 늦게 받거나 실패로 인식할 수 있다.

그러나 예약 저장이 핵심 성공 조건이고 이메일과 분석이 후속 작업이라면 책임을 분리할 수 있다.

async function createReservationFlow(
  input: CreateReservationInput
) {
  const reservation =
    await reservationRepository.create(
      input
    );

  await eventQueue.publish({
    type: "reservation.created",
    reservationId: reservation.id,
  });

  return reservation;
}

별도의 작업자가 이벤트를 처리한다.

async function handleReservationCreated(
  event: ReservationCreatedEvent
) {
  const results =
    await Promise.allSettled([
      sendConfirmationEmail(
        event.reservationId
      ),
      trackReservationCreated(
        event.reservationId
      ),
    ]);

  recordBackgroundTaskResults(
    results
  );
}

Promise.allSettled는 각 작업의 성공과 실패를 개별적으로 확인할 수 있게 한다.

하지만 모든 후속 작업을 무조건 백그라운드로 보내서는 안 된다.

예약 데이터 저장과 좌석 확보처럼 함께 성공해야 하는 작업은 하나의 비즈니스 경계에서 처리해야 한다. 확인 이메일처럼 잠시 늦어도 예약 자체의 유효성에 영향을 주지 않는 작업만 분리할 수 있다.

비동기 처리는 책임을 없애는 방법이 아니다. 책임을 언제, 어디서 완료할 것인지 명확하게 나누는 방법이다.


비동기 상태는 화면의 사용자 흐름으로 이어진다

예약 화면에서는 비동기 상태에 따라 사용자의 다음 행동이 달라진다.

function ReservationSlots({
  state,
}: {
  state: ReservationSlotState;
}) {
  switch (state.status) {
    case "idle":
      return (
        <p>예약 날짜를 선택해 달라.</p>
      );

    case "loading":
      return (
        <p>예약 가능 시간을 조회하고 있다.</p>
      );

    case "error":
      return (
        <div>
          <p>{state.message}</p>
          <button onClick={retry}>
            다시 시도
          </button>
        </div>
      );

    case "success":
      if (state.slots.length === 0) {
        return (
          <p>
            선택한 날짜에는 예약 가능한
            시간이 없다.
          </p>
        );
      }

      return (
        <SlotList slots={state.slots} />
      );
  }
}

이 코드에서 빈 목록과 오류는 다른 상태다.

  • 빈 목록은 조회가 성공했지만 가능한 시간이 없다는 뜻이다.
  • 오류는 예약 가능 여부를 확인하지 못했다는 뜻이다.

두 경우 모두 화면에 아무것도 표시하지 않으면 사용자에게는 동일하게 보인다. 그러나 다음 행동은 다르다.

빈 목록에서는 다른 날짜를 선택해야 한다. 오류에서는 같은 날짜를 다시 조회할 수 있어야 한다.

비동기 상태 설계는 기술적 상태를 사용자 언어와 행동으로 번역하는 과정이다.


비동기 작업은 서비스 전체를 이동한다

예약 가능 시간 조회와 예약 제출은 하나의 함수 안에서 끝나지 않는다.

flowchart LR
    A[날짜 선택] --> B[조회 요청 시작]
    B --> C[로딩 상태]
    B --> D[서버 검증]
    D --> E[예약 데이터 조회]
    E --> F[응답 검증]
    F --> G[예약 시간 표시]
    G --> H[예약 제출]
    H --> I[서버의 최신 상태 확인]
    I --> J[예약 생성]
    J --> K[완료 화면]
    J --> L[이메일·후속 작업]
    B -. 취소·시간 초과 .-> M[복구 흐름]
    H -. 실패·결과 불확실 .-> M

각 구간에는 서로 다른 책임이 있다.

  • 프론트엔드는 로딩, 성공, 오류와 취소 상태를 표현한다.
  • API 계층은 입력과 응답 형식을 검증한다.
  • 서버는 최신 예약 상태를 기준으로 작업 가능 여부를 판단한다.
  • 데이터베이스는 예약 결과의 Source of Truth를 유지한다.
  • 후속 작업 시스템은 이메일과 알림의 실패를 추적하고 재처리한다.
  • 로그와 모니터링은 느린 요청과 반복 실패를 발견한다.

비동기 작업을 함수 하나의 실행 방식으로만 이해하면 이 흐름에서 발생하는 중간 상태와 책임이 보이지 않는다.


비동기 작업을 설계하기 전에 물어봐야 할 질문

작업의 결과가 어디에 필요한가

  1. 이 작업이 완료되어야 다음 단계로 이동할 수 있는가?
  2. 결과가 없어도 다른 화면이나 작업을 계속 사용할 수 있는가?
  3. 사용자가 기다리는 동안 어떤 상태를 보여줘야 하는가?
  4. 빈 결과와 실패를 구분하고 있는가?
  5. 결과가 더 이상 필요하지 않을 때 요청을 취소할 수 있는가?

작업의 순서가 올바른가

  1. 다음 작업이 이전 작업의 결과에 의존하는가?
  2. 독립적인 작업을 불필요하게 순차 실행하고 있지는 않은가?
  3. 동시에 시작하면 서버나 외부 시스템에 과도한 부하가 생기지 않는가?
  4. 시작 순서와 완료 순서가 달라도 안전한가?
  5. 오래된 응답이 최신 상태를 덮어쓸 수 있는가?

실패와 복구 방법이 분명한가

  1. 네트워크 오류와 입력 오류를 구분하는가?
  2. 시간 초과가 작업 미실행을 의미하지 않을 수 있음을 고려했는가?
  3. 사용자가 안전하게 다시 시도할 수 있는가?
  4. 재시도가 중복 예약을 만들 가능성은 없는가?
  5. 실패한 후속 작업을 추적하고 다시 처리할 수 있는가?

데이터의 신뢰성과 최신성을 확인했는가

  1. 외부 입력과 API 응답을 런타임에서 검증하는가?
  2. 화면의 예약 가능 상태가 오래된 복사본일 수 있음을 고려했는가?
  3. 최종 판단은 서버의 최신 데이터로 수행하는가?
  4. 원본 데이터와 화면에서 계산한 상태를 구분하는가?
  5. 요청 완료 후 현재 선택값이 달라졌는지 확인하는가?

사용자 경험과 운영을 함께 고려했는가

  1. 로딩 중 버튼과 입력을 어떻게 처리하는가?
  2. 취소와 실제 실패를 다른 상태로 기록하는가?
  3. 느린 요청과 시간 초과를 모니터링하는가?
  4. 오류 메시지가 사용자의 다음 행동을 알려주는가?
  5. 요청 ID나 작업 ID로 실패를 추적할 수 있는가?

흔한 실수는 비동기 흐름의 책임을 숨긴다

await를 사용하면 모든 순서가 안전하다고 생각한다

const slots =
  await loadReservationSlots(date);

setReservationSlots(slots);

함수 내부의 순서는 읽기 쉬워졌지만 사용자가 그동안 날짜를 변경하지 않았다는 보장은 없다.

응답을 적용하기 전에 현재 선택값을 확인하거나 이전 요청을 취소해야 한다.


로딩 상태를 boolean 하나로만 표현한다

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

isLoading만으로는 조회 전, 성공, 빈 결과, 오류와 취소를 명확하게 구분하기 어렵다.

가능한 상태를 구별된 타입으로 표현하면 동시에 존재할 수 없는 상태 조합도 줄일 수 있다.


모든 요청을 순차적으로 실행한다

const profile =
  await getCurrentUserProfile();

const policies =
  await getReservationPolicies();

두 결과가 서로 독립적이라면 불필요하게 전체 대기 시간이 길어진다.

작업 사이의 의존성과 실패 정책을 확인한 뒤 동시에 시작할 수 있다.


모든 작업을 Promise.all로 실행한다

await Promise.all([
  createReservation(input),
  sendConfirmationEmail(),
]);

이메일은 생성된 예약 ID가 필요할 수 있고 예약 생성이 실패하면 전송해서는 안 된다.

동시에 실행할 수 있다는 사실과 동시에 실행해도 된다는 판단은 다르다.


시간 초과를 작업 실패로 단정한다

catch {
  showMessage("예약에 실패했다.");
}

응답을 받지 못했더라도 서버에서는 예약이 생성되었을 수 있다.

예약 결과 조회, 작업 ID와 멱등성 키를 사용해 실제 상태를 확인해야 한다.


응답 타입을 강제 변환해 검증을 생략한다

const slots =
  (await response.json())
    as ReservationSlot[];

타입 단언은 런타임 데이터의 구조를 보장하지 않는다.

서비스 외부에서 들어온 값은 스키마를 사용해 검증해야 한다.


버튼 비활성화만으로 중복 예약을 막는다

<button disabled={isSubmitting}>
  예약하기
</button>

여러 탭, 재시도와 다른 클라이언트에서의 요청은 막을 수 없다.

서버가 멱등성, 최신 상태 확인과 데이터베이스 제약을 통해 중복 실행을 방지해야 한다.


비동기의 핵심은 시간 속에서 작업의 책임을 유지하는 데 있다

비동기를 처음 배울 때는 다음 코드만으로 핵심 동작을 이해할 수 있다.

const slots =
  await loadReservationSlots(date);

작업이 완료되면 결과를 받아 다음 코드를 실행한다는 설명은 입문 단계에서 충분하다.

하지만 실제 서비스에서는 작업이 시작된 뒤 완료되기 전까지 시간이 존재한다. 그동안 사용자가 다른 행동을 할 수 있고, 다른 요청이 먼저 끝날 수 있으며, 서버 데이터가 바뀔 수 있다.

따라서 다음 질문까지 답해야 한다.

  • 결과를 기다리는 동안 무엇을 보여줄 것인가?
  • 작업은 순서대로 실행해야 하는가, 동시에 시작해도 되는가?
  • 오래된 응답을 어떻게 구분할 것인가?
  • 필요 없어진 작업을 어떻게 취소할 것인가?
  • 시간 초과와 실제 실패를 어떻게 구분할 것인가?
  • 실패한 작업을 다시 시도해도 안전한가?
  • 예약이 실행되었는지 알 수 없을 때 무엇을 Source of Truth로 확인할 것인가?
  • 핵심 작업과 후속 작업의 책임을 어디에서 나눌 것인가?

비동기 처리는 기다림을 없애지 않는다. 기다리는 동안 서비스가 어떤 상태에 있으며, 완료와 실패 이후 무엇을 해야 하는지를 명확하게 만든다.

비동기는 기다리지 않고 다음 코드를 실행하는 것이 아니다.

시간이 걸리는 작업의 순서와 상태, 실패와 복구를 관리하는 방법이다.

profile
Vision eXperience Developer

0개의 댓글