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

백오피스는 하루 여덟 시간 쓰는 화면이다

숙박 앱 관리자 시스템을 만들면서 알게 된 것들입니다. 개발자가 아닌 사람이 하루 종일 켜 놓고 쓰는 화면은 만드는 기준이 다릅니다.

#백오피스#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건이 함께 숨겨집니다"가 낫습니다.
  • 이력을 남깁니다. 쿠폰 발급 이력을 남겨 두니 "이 쿠폰 누가 만들었냐"는 질문이 조회로 끝났습니다.

화려한 기능은 하나도 없지만, 이런 것들이 쌓이면 쓰는 사람의 하루가 조금씩 가벼워진다고 생각합니다.

이 글이 나온 작업