본문으로 건너뛰기
DEVKBS
개발 노트
협업읽는 데 약 6

백오피스는 남의 하루를 줄여주는 일이다

숙박 앱 관리자 시스템을 만들며 배운 것. 개발자가 아닌 사람이 매일 여덟 시간 쓰는 화면에서는 기준이 달라집니다.

#백오피스#UX#권한#React

백오피스는 포트폴리오에 넣기 애매한 작업으로 취급되곤 합니다. 화려하지 않고, 스크린샷이 예쁘지 않고, 사용자 수도 적습니다. 그런데 숙박 앱 관리자 시스템을 만들면서 생각이 바뀌었습니다. 사용자 수는 적지만 한 사람이 하루 여덟 시간을 이 화면에서 보냅니다.

일반 서비스는 사용자가 몇 분 머물다 나갑니다. 백오피스는 아닙니다. 사업자는 아침에 이 화면을 띄우고 퇴근할 때까지 켜 둡니다. 클릭 하나를 줄이는 것이 하루에 백 번 반복되고, 실수 하나가 실제 예약과 돈에 영향을 줍니다.

1. 등록하고 앱을 켜서 확인하는 왕복을 없앤다

이 프로젝트에서 가장 값어치 있었던 기능은 기술적으로 가장 쉬운 것이었습니다.

사업자가 숙소 정보를 등록하는 폼이 있었습니다. 이름, 설명, 사진, 편의시설을 입력합니다. 문제는 이걸 다 입력하고 저장한 다음, 실제 앱을 켜서 어떻게 보이는지 확인하고, 이상하면 다시 백오피스로 돌아와 고치는 왕복이 계속된다는 것이었습니다. 설명이 두 줄에서 잘리는 걸 등록 후에야 알게 됩니다.

그래서 폼 오른쪽에 실제 앱과 같은 레이아웃의 프리뷰를 붙였습니다. 입력하는 즉시 반영됩니다.

tsx폼 값을 그대로 받아 앱 레이아웃으로 그린다
const StayForm = () => {
  const { watch, register } = useFormContext<StayInput>();
  const values = watch(); // 입력할 때마다 갱신

  return (
    <div className="grid gap-8 lg:grid-cols-[1fr_360px]">
      <fieldset>{/* 입력 필드들 */}</fieldset>

      {/* 앱과 동일한 폭·타이포·잘림 규칙으로 그린다 */}
      <aside className="lg:sticky lg:top-6">
        <AppPreview stay={values} />
      </aside>
    </div>
  );
};

등록 후 다시 고치는 일이 눈에 띄게 줄었습니다. 어려운 기술을 쓴 게 아니라, 사용자가 반복하던 왕복을 발견하고 없앤 것뿐입니다.

2. 권한은 숨기는 게 아니라 나누는 것

운영 관리자와 입점 사업자가 같은 시스템을 쓰되 다른 것을 봐야 했습니다. 흔한 방식은 컴포넌트 안에서 분기하는 것입니다.

tsx화면 수가 늘면 감당하기 어려워진다
const StayDetail = () => {
  const { role } = useAuth();
  return (
    <>
      <StayInfo />
      {role === "admin" && <SettlementPanel />}
      {role === "admin" && <DeleteButton />}
      {(role === "admin" || role === "owner") && <EditButton />}
      {/* 화면마다 이런 조건문이 흩어진다 */}
    </>
  );
};

이 방식의 문제는 코드가 지저분해지는 것보다, '이 권한은 무엇을 볼 수 있는가'라는 질문에 답할 수 없게 된다는 것입니다. 답하려면 모든 컴포넌트를 열어 조건문을 세어야 합니다.

그래서 라우트 트리 자체를 권한별로 나눴습니다. 메뉴 목록도 이 정의에서 파생시켰습니다.

ts권한이 무엇을 볼 수 있는지 한 파일에서 답이 나온다
export const routesByRole = {
  admin: [
    { path: "/stays", label: "숙소 관리" },
    { path: "/reservations", label: "예약 현황" },
    { path: "/coupons", label: "쿠폰" },
    { path: "/settlements", label: "정산" },
    { path: "/stats", label: "통계" },
  ],
  owner: [
    { path: "/stays", label: "내 숙소" },
    { path: "/reservations", label: "예약 현황" },
  ],
} satisfies Record<Role, RouteDef[]>;

컴포넌트는 자기가 어떤 권한에서 렌더되는지 모릅니다. 권한이 하나 추가됐을 때도 손댈 곳이 명확했습니다.

3. 자주 하는 질문의 형태로 화면을 만든다

예약 현황을 처음에는 표로 만들었습니다. 객실명, 예약자, 체크인, 체크아웃, 상태가 열로 늘어선 흔한 테이블입니다. 데이터는 다 있었습니다.

그런데 운영자가 가장 자주 하는 질문은 "이번 주에 어느 방이 비어 있나"였습니다. 이 질문에 표로 답하려면 스크롤을 오르내리며 날짜를 눈으로 맞춰야 합니다. 데이터는 있는데 답이 안 나오는 화면이었습니다.

객실을 행으로, 날짜를 열로 놓는 캘린더 뷰로 바꾸니 빈 방이 그냥 보였습니다. 같은 데이터인데 형태만 바꾼 것입니다.

화면은 데이터를 보여주는 곳이 아니라 질문에 답하는 곳입니다.

백오피스를 만들 때 나는 이제 기능 목록보다 먼저 이걸 묻습니다. 이 사람이 하루에 가장 자주 하는 질문 세 가지는 무엇인가? 그 세 개에 가장 빨리 답하는 화면을 만들고, 나머지는 그다음입니다.

4. 무거운 작업은 진행 상태를 보여준다

사업자가 홍보용 쇼츠 영상을 올리는 기능이 있었습니다. 스마트폰으로 찍은 원본이 수백 MB라 그대로 올리면 오래 걸립니다. 그래서 브라우저에서 FFmpeg로 미리 규격을 낮춰 보내기로 했습니다.

처음에는 변환 중에 스피너만 띄웠는데, 사용자들이 멈춘 줄 알고 창을 닫았습니다. 진행률을 퍼센트로 노출하자 이 문제가 사라졌습니다. 걸리는 시간은 똑같았습니다.

ts변환 진행률을 그대로 UI로 흘린다
ffmpeg.on("progress", ({ progress }) => {
  setProgress(Math.round(progress * 100));
});

5. 되돌릴 수 있게 만든다

일반 서비스에서는 사용자가 실수해도 대개 본인만 곤란합니다. 백오피스에서는 다릅니다. 잘못 발급한 쿠폰이 나가고, 잘못 지운 객실 때문에 예약이 사라집니다.

  • 삭제는 즉시 지우지 않고 상태 변경으로 처리합니다. 복구할 수 있어야 합니다.
  • 되돌릴 수 없는 조작은 확인 단계에서 '무엇이 사라지는지'를 구체적으로 보여줍니다. "정말 삭제하시겠습니까?"보다 "객실 3개와 예약 12건이 함께 숨겨집니다"가 낫습니다.
  • 이력을 남깁니다. 쿠폰 발급 이력을 남겨 두니 "이 쿠폰 누가 만들었냐"는 질문이 조회로 끝났습니다.

정리

  • 사용자 수가 적어도 사용 시간이 길습니다. 클릭 하나가 하루에 백 번 반복됩니다.
  • 사용자가 반복하는 '왕복'을 찾아 없앱니다. 기술적으로 쉬운 기능이 가장 큰 효과를 내는 경우가 많습니다.
  • 권한은 컴포넌트에 흩뿌리지 말고 라우트 수준에서 나눕니다. 단, 화면에서 숨기는 것은 보안이 아닙니다.
  • 기능 목록보다 '이 사람이 가장 자주 하는 질문'에서 화면을 시작합니다.
  • 오래 걸리는 작업은 진행 상태를 노출합니다.
  • 실수를 되돌릴 수 있게 만듭니다. 여기서의 실수는 실제 돈과 예약에 닿습니다.

이 글이 나온 작업