고객 여정도메인 설계테크 딥다이브

쇼핑몰 팀 프로젝트 ②-1 고객 여정에서 도메인과 데이터 요구로 — 화면의 명사와 동사가 테이블 목록이 되기까지

· 2026-10-20 발행· 28분 읽기

목차

마케터로 일하던 시절 저는 고객 여정 지도를 수십 장 그렸어요. 인지, 탐색, 구매, 재구매. 단계마다 고객이 무엇을 보고 무엇을 망설이는지 적고, 그 옆에 우리가 보낼 메시지를 붙였죠. 그런데 쇼핑몰 백엔드를 설계하려고 앉은 첫날, 그 지도가 그대로 데이터 요구서가 된다는 걸 몰랐어요. 테이블 이름부터 떠올리다가 막혔거든요.

이 글은 그 순서를 뒤집는 글이에요. 손님의 여정 여섯 단계(발견 → 탐색 → 결정 → 담기 → 주문 → 확인)에서 출발해 네 가지를 끌어냅니다. 첫째, 도메인 — 백엔드를 어떤 업무 경계로 자르고 각 경계가 무엇을 소유하는가. 둘째, 명사와 동사 — 각 단계의 화면이 저장돼 있어야 그려지는 것과 버튼이 DB(데이터베이스)에 시키는 일. 셋째, 트랜잭션 경계 — 어떤 동사들이 함께 성공하거나 함께 실패해야 하는가. 넷째, 접근 경로 — 읽기와 쓰기가 각각 어느 문으로 DB에 닿는가. 다 읽고 나면 여정 한 단계를 받아 “도메인 하나, 테이블 이름 몇 개, 동작 몇 개”로 적는 체크리스트를 손에 쥐게 될 거예요.

1편에서 세운 스펙 표의 “보이는 데이터” 열이 이 글의 출발점이고, 그 명사들을 표와 관계선으로 굳히는 일(표기법, 키, 정규화, 스냅샷, ERD — 개체-관계 다이어그램, 테이블과 관계선을 그린 그림 — 의 이력)은 ②-2에서 이어집니다. 여기서는 “어떤 테이블과 어떤 동작이 필요한가”까지만 가요.

이 글의 지도. 맨 위에 발견부터 확인까지 여정 여섯 칸, 그 아래 도메인·명사와 동사·트랜잭션 경계·읽기 쓰기 경로 네 상자, 그 아래 판정 체크리스트 띠와 ②-2 로 넘기는 점선 상자, 맨 아래 10-13 부터 10-20 까지 내 작업 타임라인 이 글의 지도. 여정에서 출발해 네 부품을 지나 테이블 목록 앞에서 멈춘다

백엔드 설계는 왜 테이블이 아니라 고객 여정에서 시작할까?

데이터 요구는 테이블에서 나오지 않고 손님의 행동에서 나온다. 여정의 단계마다 화면이 있고, 화면마다 보여야 할 것과 바꿔야 할 것이 있으며, 그것을 적은 목록이 곧 백엔드가 소유해야 할 데이터와 동작의 목록이다.

왜 이 순서냐면요. 테이블 이름부터 적으면 빠진 열과 빠진 표는 화면을 만드는 도중에야 하나씩 튀어나와요. 반대로 손님이 지금 무엇을 하려 하는지에서 출발하면, 저장할 것과 바꿀 것이 화면에 이미 적혀 있어요. 수업에서 배운 데이터 모델링 절차도 같은 말을 해요. 네 단계 중 첫 단계가 업무 파악이고, 그 가장 좋은 방법으로 화면을 의뢰인과 함께 확인하는 것을 꼽았거든요. 두 번째 단계가 무엇과 무엇이 관계를 맺는지 그리는 개념 설계고요. 이 글은 그 두 단계만 다룹니다. 열의 타입과 키를 정하는 세 번째 단계부터는 ②-2예요.

튜터가 돌려준 피드백에도 같은 원칙이 있었어요. ERD가 완성됐다고 FK(외래키, 다른 표의 행을 가리키는 열) 연결만 확인하고 끝내지 말고, 실제 손님의 행동이 DB에서 어떻게 표현되는지를 시나리오로 따라가 보라는 것. 가입해서 판매자가 되고 상품을 올리는 길, 상품을 골라 장바구니에 넣고 주문하는 길, 쿠폰을 쓰고 취소하는 길. 여정은 설계의 입력이면서 동시에 설계를 검증하는 시험지예요.

절차는 여섯 칸이에요. 여정 단계 하나를 고르고, 그 단계의 화면을 찾고, 화면에서 명사와 동사를 읽고, 명사의 주인이 되는 도메인을 정하고, 동사 가운데 함께 성공해야 하는 것들을 한 경계로 묶고, 읽기와 쓰기가 지나갈 문을 정한다. 그리고 다음 단계로 넘어가 같은 여섯 칸을 채웁니다. 여섯 단계를 다 돌면 도메인별 테이블 이름 목록과 동작 목록이 남고, 그것이 ②-2의 입력이에요.

전체 절차. 여정 단계, 화면, 명사와 동사, 도메인, 경계, 경로 여섯 상자가 화살표로 이어지고 점선이 처음으로 되돌아간다. 아래에 이 글의 산출물 상자와 ②-2 의 입력 상자 여정 한 단계를 여섯 칸에 통과시키고, 다음 단계로 돌아간다

마케팅의 고객 여정은 개발의 흐름과 어디가 같고 어디가 다를까?

고객 여정은 손님이 구매 전부터 후까지 지나는 단계를 생각·행동·접점과 함께 시간순으로 그린 지도이고, 개발의 사용자 흐름은 같은 단계를 화면과 데이터와 동작으로 다시 적은 것이다. 단계는 같고 적는 열이 다르다.

퍼널로 치면 여정의 전환 이벤트가 곧 쓰기 동사예요. 마케팅에서 퍼널 분석은 방문 → 상세 → 장바구니 → 결제로 내려가며 어디서 가장 많이 새는지를 재는 정량 도구인데, 그 단계 사이의 “전환”이 개발에서는 DB에 행이 생기는 순간이거든요. 장바구니 전환율을 올리고 싶다는 마케터의 말과 장바구니 표에 행을 쓰는 서버 액션이 필요하다는 개발자의 말은 같은 사건을 가리켜요.

하나 더 빌려 올 도구가 서비스 블루프린트예요. 여정 아래에 가시선을 긋고, 손님이 보는 앞무대와 보지 못하는 뒷무대, 그 뒤의 지원 프로세스를 층으로 그리는 방식이죠. 이 글에서는 그 네 줄을 손님 행동, 화면, 서버, DB로 바꿔 씁니다. 한 가지 밝혀 둘 것은, 제 위키의 블루프린트 페이지는 전용 출처 없이 일반 지식으로 적은 것이라 신뢰도를 낮게 표시해 두었다는 점이에요. 여기서는 “줄을 네 개 긋는다”는 뼈대만 빌립니다.

표기는 두 표의 열을 나란히 두면 보여요.

마케팅의 여정 지도 개발의 흐름 표
행 단계 (인지 · 탐색 · 구매 · 재구매) 단계 (발견 · 탐색 · 결정 · 담기 · 주문 · 확인)
열 1 손님의 행동 화면 (URL)
열 2 생각 · 감정 보이는 데이터 (명사)
열 3 접점 · 채널 버튼이 시키는 일 (동사)
빠지는 것 뒷무대 감정

저희 팀 쇼핑몰(레퍼런스 패션 편집몰 하나를 따라 만들고 있어요)의 여정을 개발의 열로 적으면 이래요. 화면 열은 1편의 스펙 표에서 가져왔고, 그 표에 없던 주문과 확인 두 단계는 경계를 정하기 위해 추가했어요.

단계 화면 보이는 데이터 (명사) 손님이 하는 일 (동사)
발견 홈 / 배너 · 카드(이미지 · 제목 · 태그) 훑는다 — 읽기
탐색 목록 (저장소에 아직 없음) 카드 그리드 · 정렬 · 품절 · 할인율 거르고 정렬한다 — 읽기
결정 상세 /product/[id] 브랜드 · 이름 · 정가 · 판매가 · 이미지 여러 장 · 사이즈 · 품절 사진을 넘기고 사이즈를 고른다 — 읽기
담기 상세의 버튼 → /cart 상품 줄 · 수량 · 합계 · 빈 상태 담는다 — 쓰기
주문 주문서 (범위 밖) 수령인 · 주소 · 메모 · 총액 · 주문 줄 주문한다 — 쓰기 여러 개
확인 주문 내역 (범위 밖) 주문 번호 · 상태 · 줄 본다 — 읽기, 내 것만

여기서 처음 보이는 것이 있어요. 앞의 세 단계는 전부 읽기이고, 쓰기는 담기에서 처음 나타나며, 주문에서 한꺼번에 여러 개가 터진다는 것. 이 분포가 뒤에 나올 도메인과 경계와 경로를 거의 결정합니다.

블루프린트식 네 줄. 손님 행동, 화면, 서버, DB 네 줄에 발견부터 주문까지 다섯 열이 있고, 가시선과 내부 상호작용선이 점선으로 그어져 있다. 담기와 주문 열의 서버 칸만 서버 액션으로 강조돼 있다 손님이 보는 줄은 맨 위 하나뿐이고, 설계가 할 일은 그 아래 세 줄을 채우는 것이다

도메인은 어떤 기준으로 자를까 — 업무 경계?

도메인은 함께 바뀌는 이유가 하나인 업무의 묶음이고, 그 업무가 소유하는 데이터와 동작을 한 폴더에 둔 것이다. 자르는 질문은 “이 데이터가 바뀌는 이유가 누구의 업무에 있는가”다.

마케팅 조직으로 치면 데이터를 소유하는 부서예요. 가격을 바꿀 권한은 상품팀에 있고, 주문 상태를 바꿀 권한은 주문팀에 있죠. 다른 팀이 그 값을 읽는 건 자유지만, 바꾸려면 소유 부서의 절차를 거쳐야 해요. 도메인도 같아요. 재고는 상품 도메인의 것이지만 주문 도메인이 “주문이 들어왔으니 줄여 달라”고 부르는 구조예요.

수업에서 배운 폴더 규칙은 세 갈래였어요. 주소는 app/, 업무는 domains/, 공용은 shared/. 그리고 도메인 하나의 안쪽은 타입, 데이터 접근(읽기), 서버 액션(쓰기), 화면 부품으로 나눠요. 이 규칙을 설명한 위키 페이지는 같은 수업의 두 자료가 근거라 독립 확증이 아니고 신뢰도가 낮다고 표시해 두었는데, 저희 팀은 그 규칙을 그대로 따르고 있어서 이 글에서는 팀의 약속으로 씁니다.

domains/<업무>/
  types.ts        이 업무의 데이터 모양
  api/            읽기 — 서버 컴포넌트가 부른다
  actions/        쓰기 — 서버 액션, "use server"
  components/     이 업무의 화면 부품

여정 여섯 단계를 이 기준으로 자르면 도메인은 넷이에요. 각 도메인이 무엇을 소유하는지, 그리고 10월 20일 기준 저희 저장소에 실제로 무엇이 있는지를 같은 표에 적었어요. 저장소 쪽은 폴더와 파일을 제가 직접 센 것이고, 소유 열은 10월 19일에 제가 정리한 연결 계획 기준이에요. 표에 나오는 mock(진짜 DB 대신 손으로 적은 가짜 데이터), JSON(사람이 읽을 수 있는 데이터 파일 형식), JWT(서명된 토큰, 로그인 상태를 쿠키에 담는 방식)는 전부 DB가 오기 전의 임시 자리예요.

도메인 소유하는 데이터 소유하는 동작 저장소 현재 (10-20)
상품 products 상품 · 브랜드 · 이미지 · 옵션(사이즈 · 추가금 · 재고) 목록 읽기 · 상세 읽기 · 재고 차감 types.ts · api/products.ts(mock 변환) · 화면 5개 중 2개는 0바이트
회원 auth 프로필 · 세션 · 요청자가 누구인가 가입 · 로그인 · 로그아웃 서버 액션 · 세션(JWT 쿠키) · 임시 회원 파일(JSON)
장바구니 cart 회원 × 옵션 × 수량 · 선택 여부 담기(있으면 더하기) · 삭제 · 내 줄 읽기 화면 부품 3개 · 이미지 5장 — 쓰기 함수 없음
주문 orders 주문 머리 · 주문 줄 · 주문 당시 가격 복사본 주문 생성 · 내 주문 읽기 폴더 없음

표의 오른쪽 열을 왼쪽 두 열과 견주면 지금 팀이 어디까지 왔는지가 보여요. 읽기는 상품 도메인에 있고, 쓰기는 회원 도메인에만 있고, 장바구니는 모양뿐이고, 주문은 시작도 안 했어요.

여정 여섯 칸에서 네 도메인 상자로 화살표가 내려온다. 발견·탐색·결정은 상품으로, 담기는 장바구니와 회원으로, 주문은 주문으로 가되 점선으로 상품과 장바구니도 함께 가리킨다 단계는 시간순이지만 도메인은 겹쳐 쓰인다. 주문 한 단계가 세 도메인을 건드린다

화면에서 명사와 동사는 어떻게 읽어 낼까?

명사는 저장돼 있어야 그 화면이 그려지는 것이고, 동사는 그 화면의 버튼이 DB에 시키는 일이다. 명사마다 “어느 도메인의 어느 표”를, 동사마다 “읽기인가 쓰기인가, 누가 할 수 있나”를 붙이면 그 화면의 데이터 요구가 끝난다.

장부로 치면 명사는 칸이고 동사는 기입이에요. 칸이 없으면 적을 수 없고, 기입 규칙이 없으면 아무나 아무렇게나 적게 되죠.

표기는 두 줄짜리예요. 명사는 도메인.표.열로, 동사는 읽기 | 쓰기 · 누가 · 무엇을 바꾸나로 적어요. 그리고 명사 가운데 지금 화면에 적혀 있지만 아직 어느 표에도 없는 것은 “상수”나 “자리만”으로 따로 표시합니다. 그 표시가 다음 작업 목록이 되거든요.

제 담당이었던 상품 상세 화면의 구매 패널로 해 볼게요. 아래 캡처는 10월 20일에 저장소 복사본을 띄워 찍은 제 화면이고, 사진 영역은 잘라 냈어요.

상품 상세 구매 패널 캡처. 브랜드명과 상품명, 쿠폰과 익스클루시브 뱃지, 취소선 정가 259,000 과 빨간 5% 와 판매가 246,050, 최대 혜택가 18% 212,380원, 추가 혜택·결제수단 할인·배송 정보 세 줄, 사이즈 M L XL 중 XL 은 품절, 총 상품 금액, 장바구니에 담기와 바로 구매하기 버튼 2026-10-20 복사본의 /product/3 오른쪽 구매 패널. 이 안의 글자와 숫자 하나하나가 명사 후보다

화면의 글자 · 숫자 명사 → 도메인.표.열 지금 출처
브랜드명 상품.브랜드.이름 글자 그대로 (데이터 없음)
상품명 상품.상품.이름 mock
259,000 · 246,050 상품.상품.정가 · 판매가 상수 (10-15)
5% 두 값에서 계산 — 열 아님 함수로 계산
최대 혜택가 212,380 · 18% 쿠폰 표 — 이번 여정에 없음 상수
M · L · XL(품절) 상품.옵션.사이즈 · 재고 상수 목록
찜 996+ · 리뷰 [0] · 문의 [0] 찜 · 리뷰 · 문의 표 — 없음 자리만
총 상품 금액 판매가 + 추가금 × 수량 — 열 아님 계산

동사는 둘뿐이에요. 읽기는 상품 1건, 브랜드 1건, 이미지 여러 장, 옵션 여러 개를 한 번에 가져오는 것이고 누구나 할 수 있어요. 쓰기는 “장바구니에 담기” 하나이고, 로그인한 사람만, 그것도 재고가 있는 옵션만 담을 수 있어야 해요. 사진을 넘기거나 사이즈를 고르는 건 화면 안에서 끝나는 동사라 DB에 닿지 않고요.

표에서 눈에 띄는 건 “열 아님” 두 줄이에요. 할인율과 총액은 저장하지 않고 계산해요. 10월 15일에 겪은 일인데, 화면엔 판매가 246,050원이 보이는데 총액은 정가 259,000원으로 계산되고 있었어요. 할인율을 손으로 적고 총액 기준을 따로 둔 탓이었죠. 기준이 되는 숫자는 한 출처에서 읽고 나머지는 함수로 계산한다고 그날 정리했어요. 명사 표를 적을 때 “저장할 것”과 “계산할 것”을 가르는 이유가 여기 있어요.

한 화면에서 명사와 동사를 읽어 내는 그림. 왼쪽에 상세 화면 골격, 오른쪽 위에 명사 다섯 줄이 각각 어느 표로 가는지, 오른쪽 아래에 읽기 한 줄과 쓰기 한 줄 명사는 표로, 동사는 읽기와 쓰기로. 어느 표인지 말할 수 없는 명사가 다음 작업 목록이다

어떤 동사들이 한 상자 안에 있어야 할까 — 트랜잭션 경계?

트랜잭션은 SQL 문장 여러 개를 한 상자에 넣어, 상자째 확정되거나 상자째 취소되게 만드는 경계다. 상자는 저절로 생기지 않고 코드가 열고 닫아야 한다. 무엇을 한 상자에 넣을지의 기준은 하나다. 따로 성공해도 업무가 성립하는가.

캠페인 집행으로 치면 예산 차감과 발송 기록은 한 장의 전표예요. 발송은 됐는데 예산이 안 빠졌거나, 예산은 빠졌는데 발송이 안 된 전표는 둘 다 사고죠. 전표 한 장을 반만 처리할 수는 없어요.

SQL(DB에 명령하는 언어)로는 세 단어예요. BEGIN으로 상자를 열고, 끝까지 가면 COMMIT으로 확정하고, 중간에 하나라도 실패하면 ROLLBACK으로 상자 안의 모든 변경을 되돌려요. 선언하지 않으면 대부분의 클라이언트는 문장 하나가 끝날 때마다 자동으로 확정하는데, 그게 함정이에요.

BEGIN;
INSERT INTO orders (...) ...;          -- ① 주문 머리
INSERT INTO order_items (...) ...;     -- ② 주문 줄 (가격을 복사)
UPDATE product_options SET stock = stock - 수량 ...;   -- ③ 재고 차감, CHECK (stock >= 0)
DELETE FROM cart_items WHERE ...;      -- ④ 장바구니 비우기
COMMIT;   -- 셋째 줄이 실패하면 ROLLBACK 이 첫째 줄까지 되돌린다

여정에서 “주문한다”는 한 동사인데 DB에서는 이렇게 네 문장이에요. 제가 10월 19일에 정리한 설계에서는 ①~④가 같은 장바구니 줄을 보게 해 두었고, 재고가 모자라면 앱 코드가 먼저 검사하지 않아도 재고 열에 걸어 둔 CHECK(값의 조건을 DB가 검사하는 제약, 여기서는 0 이상)가 마지막 방어선이 돼요.

말로만 하면 믿기 어려워서, 이 글을 쓰며 PGlite(서버용 자바스크립트 실행기인 Node 위에서 도는 진짜 PostgreSQL, 설치 없이 쓸 수 있는 DB)로 네 표짜리 최소 스키마를 만들고 세 가지를 돌려 봤어요. 회원 A가 재고 10인 M을 2개 주문하면 재고가 8이 되고 주문이 1건 생긴다. 회원 B가 재고 2인 L을 3개 주문하면 셋째 문장에서 CHECK가 오류를 내고, 상자 안이라 첫째 문장의 주문 행까지 함께 사라진다.

PGlite 실행 출력 캡처. 준비 단계에서 orders 0, 재고 L 2 M 10. 회원 A 주문 후 COMMIT 되어 orders 1, M 8. 회원 B 주문은 check constraint 오류와 ROLLBACK 이 찍히고 orders 1, 재고 L 2 M 8 그대로 2026-10-20, PGlite 0.5.10 (PostgreSQL 18.6). 실패한 주문은 주문 행도 재고 변화도 남기지 않았다

시나리오 orders order_items 재고 L · M 장바구니
시작 0 0 2 · 10 2줄
A 주문 (성공, COMMIT) 1 1 2 · 8 1줄
B 주문 (재고 부족, ROLLBACK) 1 1 2 · 8 1줄 — 변화 없음

경계는 “주문한다”는 동사 하나에 하나예요. 담기는 혼자 성립하는 한 문장이라 상자가 필요 없고, 결제사 호출은 DB 밖이라 상자에 넣어도 되돌릴 수 없어요. 튜터 피드백에도 외부 결제 환불은 DB 트랜잭션만으로 원자성을 보장할 수 없으니 결과를 받은 뒤 상태를 확정하라는 말이 있었어요. 상자를 넓게 잡을수록 다른 손님이 기다린다는 것도 수업에서 들었고요. 원자성과 격리성은 서로 당기는 관계라, 어느 쪽을 얼마나 양보할지가 설계예요.

트랜잭션 경계 그림. BEGIN 띠 아래 주문 머리, 주문 줄, 재고 차감, 장바구니 비움 네 상자가 한 테두리 안에 있고, 오른쪽에 경계 밖에 둘 것 목록, 아래에 경계 안과 밖에서 실패했을 때 남는 상태 비교 상자 안에서 실패하면 아무것도 남지 않고, 상자 밖에서 실패하면 유령 주문이 남는다

읽기와 쓰기는 각각 어느 문으로 DB에 닿을까?

읽기는 서버 컴포넌트가 데이터 접근 함수를 기다려 받고, 쓰기는 서버 액션이 입력을 검증한 뒤 DB 함수 하나를 부른다. 어느 행을 보고 바꿀 수 있는지는 화면이 아니라 DB의 행 단위 정책이 정한다.

도서관으로 치면 열람실과 창구예요. 책은 누구나 열람실에서 읽지만, 장부를 고치려면 창구에 신청서를 내고 직원이 대신 적어요. 손님이 금고에 직접 손을 넣는 길은 없고요.

기호로는 두 조각이에요. 읽기 쪽은 서버 컴포넌트(서버에서 그려져 브라우저로 내려가는 화면 조각)가 await getProduct(id)로 데이터 접근 함수를 기다려요. 쓰기 쪽은 파일 맨 위에 "use server"를 적은 서버 액션(폼이 제출되면 서버에서 도는 함수)이 입력을 검증하고 rpc(원격 함수 호출)로 DB 함수를 불러요.

// 읽기 — app/(shop)/product/[id]/page.tsx (서버 컴포넌트)
const product = await getProduct(id);     // domains/products/api 안쪽만 mock → DB 로 바뀐다

// 쓰기 — domains/cart/actions/cart.actions.ts
"use server";
export async function addToCartAction(prev, formData: FormData) {
  // 1. 입력 검증 (옵션 id · 수량)   2. 로그인 확인   3. DB 함수 하나를 부른다
  const { error } = await supabase.rpc("add_to_cart", { p_option_id, p_quantity });
}

수업에서 배운 세 가지가 이 그림의 뼈대예요. 첫째, 서버 액션 파일의 "use server" 선언과 async는 취향이 아니라 그 함수가 서버에서만 돈다는 것을 못 박는 장치라는 것. 둘째, Supabase(PostgreSQL을 서비스로 제공하는 백엔드)의 연결 객체는 브라우저용과 서버용 둘이고 차이는 쿠키 옵션 하나라는 것. 셋째, RLS(Row Level Security, 행 단위 보안 — 어떤 사용자가 어떤 행에 접근할 수 있는지 DB가 직접 검사하는 기능)를 켜면 기본은 거부이고 정책으로 열어 준다는 것. 그리고 쓰기는 테이블에 직접 넣는 권한을 거두고 함수 하나로만 통과시키면 모든 변경이 같은 검증을 지난다는 것. 이 중 서버 액션과 연결 객체 페이지는 제 위키에서 단일 출처에 실행 검증이 없어 신뢰도를 낮게 두었는데, 10월 19일에 제가 로컬 Supabase에서 돌려 본 것이 1차 검증이에요.

그날 확인한 것 가운데 이 절에 해당하는 줄만 옮기면 이래요.

확인 (2026-10-19, 로컬 Supabase) 결과
비로그인으로 상품 · 브랜드 · 이미지 · 옵션 한 번에 읽기 됨
비로그인으로 담기 함수 호출 · 장바구니 읽기 · 상품 수정 전부 거부
로그인 후 M 1개 담고 M 2개 다시 담기 한 줄, 수량 3
품절 옵션 · 수량 0 · 판매 종료 옵션 담기 거부
회원 B 가 회원 A 의 장바구니 읽기 · A 의 장바구니에 직접 쓰기 0줄 · 거부

데이터를 지키는 것은 키가 아니라 행 단위 정책이라는 말이 여기서 실감 났어요. 브라우저로 나가는 공개 키를 가진 사람이 DB에 직접 요청을 보내도 정책이 없는 표는 비어 있고, 함수를 통하지 않은 쓰기는 거부되니까요. 다만 수업에서 튜터가 남긴 유보도 적어 둬야 공정해요. 관리자가 남의 글을 고쳐야 하는 서비스에서는 “본인 것만”이라는 정책이 걸림돌이 되니 실무에서는 선택적으로 쓴다고 했어요. 저희 쇼핑몰은 손님 쪽 흐름만 다루니 아직 그 문제를 만나지 않았고요.

읽기 길과 쓰기 길 그림. 왼쪽 브라우저에서 위로는 서버 컴포넌트를 거쳐 읽기 정책으로, 아래로는 서버 액션을 거쳐 함수 하나로 들어가고, 브라우저에서 표로 직접 가는 점선은 막혀 있다. 오른쪽에 여덟 표 목록 문은 둘이고 쓰기 문 뒤에는 함수 하나뿐이다. 표로 직접 가는 길은 없다

여정, 도메인, 동사, 경계, 경로는 어떻게 맞물릴까?

부품이 합쳐지는 규칙은 하나예요. 여정 단계 하나에 여섯 칸을 채우고, 여섯 단계를 돌고 나면 도메인별로 모은다. 단계는 시간순이지만 도메인은 겹쳐 쓰이니 마지막에 도메인 기준으로 다시 묶어야 테이블 목록이 나와요. 그리고 쓰기 동사가 둘 이상 모인 단계에만 경계를 긋고, 모든 쓰기에 문을 하나씩 붙여요.

같은 손님을 보는 네 가지 도구를 나란히 두면 각각 무엇을 묻고 무엇을 빠뜨리는지가 보여요.

도구 묻는 질문 단위 산출물 빠지는 것
고객 여정 지도 손님이 언제 무엇을 느끼고 하나 단계 × 접점 서사가 있는 지도 뒷무대, 데이터
퍼널 분석 어느 단계에서 가장 많이 새나 단계 × 전환율 병목 하나 왜 새는지, 비선형 경로
서비스 블루프린트 접점 뒤에서 조직이 어떻게 움직이나 접점 × 층 가시선 아래 운영도 데이터의 모양
이 글의 흐름 설계 화면마다 무엇을 저장하고 무엇을 바꾸나 단계 × 명사 · 동사 도메인 · 테이블 · 동작 목록 감정, 전환율

총정리하면 이래요.

부품 묻는 질문 산출물 틀리면 생기는 일
여정 → 화면 손님이 지금 무엇을 하려 하나 단계 × 화면 표 범위 밖 화면부터 만든다
도메인 이 데이터는 누구의 업무인가 도메인 × 소유 표 한 표를 여러 폴더가 고친다
명사 · 동사 저장할 것과 바꿀 것은 무엇인가 명사 → 표, 동사 → 읽기/쓰기 화면에 상수가 박힌다
경계 무엇이 함께 성공해야 하나 한 상자에 든 문장 목록 유령 주문이 남는다
경로 읽기와 쓰기는 어느 문으로 가나 서버 컴포넌트 / 서버 액션 → 함수 브라우저가 표에 직접 쓴다

우리 쇼핑몰의 여정 여섯 단계를 이 틀로 풀면?

판정 체크리스트를 먼저 정해요. 단계마다 같은 다섯 질문을 던졌습니다.

  1. 화면 · 범위 — 그 화면이 있나, 이번 열흘의 안인가 밖인가?
  2. 명사 → 표 — 보이는 것이 어느 도메인의 어느 표인가? 없으면 “없음”으로.
  3. 동사 — 읽기인가 쓰기인가, 누가 할 수 있나?
  4. 경계 — 쓰기가 둘 이상이면 한 상자인가?
  5. 문 — 읽기는 서버 컴포넌트, 쓰기는 서버 액션 → 어느 함수인가?
단계 1 화면 · 범위 2 명사 → 표 3 동사 4 경계 5 문
발견 홈 · 안 상품 · 이미지(대표) · 브랜드 읽기 · 누구나 — 서버 컴포넌트 → 목록 함수
탐색 목록 · 없음 상품 · 옵션(품절 판정) · 이미지 읽기 · 누구나 — 서버 컴포넌트 → 목록 함수
결정 상세 · 안 상품 · 브랜드 · 이미지 · 옵션 (쿠폰 · 찜 · 리뷰는 없음) 읽기 · 누구나 — 서버 컴포넌트 → getProduct
담기 상세 버튼 → /cart · 안 장바구니 · 프로필 · 옵션(재고 확인) 쓰기 · 로그인 회원 한 문장 — 상자 없음 서버 액션 → add_to_cart
주문 주문서 · 밖 주문 · 주문 줄 · 옵션(재고) · 장바구니 쓰기 4개 · 로그인 회원 한 상자 서버 액션 → 주문 함수 (미작성)
확인 주문 내역 · 밖 주문 · 주문 줄 읽기 · 내 것만 — 서버 컴포넌트 → 내 주문 함수

2열을 도메인별로 다시 모으면 ②-2에 넘길 테이블 목록이 나와요. 회원 1(프로필), 상품 4(상품 · 브랜드 · 이미지 · 옵션), 장바구니 1, 주문 2(머리 · 줄). 여덟 개예요. 그리고 2열에서 “없음”으로 적힌 쿠폰, 찜, 리뷰, 문의, 상태 변경 이력, 카테고리는 이번 여정의 안에 없으니 ②-2에서도 “그려만 둘 것”으로 내려가요. 10월 14일에 팀이 채택한 설계는 쿠폰과 결제와 이력을 포함해 열네 개였는데, 여정으로 다시 세면 여덟 개가 먼저고 여섯 개는 다음 여정이에요.

3열과 5열을 모으면 동작 목록이에요. 읽기 넷(목록 · 상세 · 내 장바구니 · 내 주문), 쓰기 셋(담기 · 주문 · 가입/로그인). 이 중 저장소에 실제로 있는 건 상세 읽기 하나와 로그인뿐이에요. 아래는 10월 20일에 복사본에서 뽑은 domains/ 트리인데, 주문 폴더가 없다는 것이 이 표의 5행과 같은 말이에요.

domains 폴더 트리 캡처. auth 아래 lib와 actions.ts와 dal.ts, cart 아래 components와 data, main 아래 components와 data와 types.ts, products 아래 api와 components와 types.ts. 폴더마다 파일 수 주석이 있고 맨 아래 orders 없음이 빨갛게 적혀 있다 2026-10-20 저장소 복사본의 domains/ 2단계. 12 폴더, 쓰기 함수가 있는 도메인은 auth 하나뿐이다

표의 4열이 비어 있는 다섯 줄과 “한 상자”인 한 줄을 보면, 이 쇼핑몰에서 트랜잭션을 고민할 자리는 주문 하나라는 게 분명해져요. 여정 여섯 단계 중 경계가 필요한 단계는 하나뿐이고, 그 하나가 세 도메인을 동시에 건드린다는 것이 이 표의 결론이에요.

처음 설계는 무엇이 틀렸고 어떻게 고쳤나?

10월 13일 첫날 제 메모는 명사 목록이었어요. 회원, 상품, 주문, 장바구니, 결제, 배송, 사입처. 떠오르는 대로 적었고, 그걸로 ERD 한 장을 그렸죠. 그 그림은 상품 한 표에 열이 열한 개였고, 사이즈와 재고와 쿠폰까지 그 안에 있었어요. 작업일지와 회고에 남은 결정으로 지금의 생각과 견주면 다섯 군데가 달라요.

항목 전 (10-13) 무엇이 틀렸나 후 (10-16 ~ 10-20)
출발점 떠오르는 명사 7개 결제 · 배송 · 사입처는 이번 여정의 어느 화면에도 없다. 범위 밖 명사가 범위 안 표를 복잡하게 만든다 여정 6단계 → 화면 → 명사. 없는 화면의 명사는 “다음 여정”
단위 장바구니 줄이 상품을 가리킴 손님은 상품이 아니라 사이즈를 담는다. 재고도 사이즈별이다 담기 · 주문 · 재고의 단위는 옵션. 상품 1 : 옵션 N
동사 담기 = /cart 로 이동 (10-16) 저장이 없으니 새로고침하면 사라진다. 주문은 생각하지 않았다 담기 = (회원, 옵션) 한 줄, 있으면 더하기. 주문 = 네 문장 한 상자
출처 화면 상수 259,000 (10-15) 표시 가격과 총액 기준이 어긋났다 정가 · 판매가 두 열에서 읽고 할인율은 계산
문 페이지가 mock 을 직접 읽음 출처가 바뀌면 화면마다 고친다. 쓰기 문이 없다 읽기는 서버 컴포넌트 → 접근 함수, 쓰기는 서버 액션 → DB 함수

두 번째 줄이 가장 늦게 보였어요. 10월 16일 회고에서 “장바구니가 상품을 참조하는 판과 옵션을 참조하는 판이 문서에 둘 다 있다”고 팀에 확인할 사항으로 적어 두었는데, 여정의 담기 단계를 화면으로 다시 보니 답이 거기 있었어요. 손님이 누르는 건 M이지 상품이 아니거든요. 명사 목록에서 출발했을 때는 “장바구니”와 “상품”이라는 단어가 먼저 왔고, 화면에서 출발하니 “M을 2개”가 먼저 왔어요.

교정 전후. 왼쪽 점선 상자에 10-13 의 출발점·단위·동사·가격·경계 다섯 항목, 오른쪽 상자에 10-16 부터 10-20 까지의 결정 다섯 항목이 여정·단위·동사·출처·문 다섯 화살표로 대응된다 다섯 줄 모두 “단어에서 출발”이 “손님의 행동에서 출발”로 바뀐 것이다

직접 겪은 함정은 무엇이었나?

전부 제 작업 기록과 이 글을 쓰며 돌린 실행에서 나온 것이에요.

1. 경계를 선언하지 않으면 문장마다 확정된다. 위의 PGlite 실험에서 같은 네 문장을 상자 없이 하나씩 실행해 봤어요. 셋째 문장(재고 차감)만 실패했는데 앞의 두 문장은 이미 확정돼 있었고, 결과는 주문 2건, 주문 줄 2건, 재고는 그대로. 재고를 받지 않은 유령 주문 하나가 남은 거예요. 오류 메시지는 똑같이 떴기 때문에 로그만 보면 “실패했으니 안 들어갔겠지”라고 생각하기 쉬워요.

PGlite 실행 출력 캡처. 경계 없이 자동 커밋으로 같은 네 문장을 하나씩 실행하자 check constraint 오류가 뜨고, 결과는 orders 2, order_items 2, 재고는 L 2 M 8 그대로. 아래에 유령 주문이 남았다는 줄 같은 실행의 뒷부분. 오류는 같은데 남는 상태가 다르다

2. upsert는 더하지 않는다. 10월 19일 기록에 따르면, 장바구니 담기를 Supabase 클라이언트의 upsert(없으면 넣고 있으면 갱신)로 하면 M을 1개 담고 다시 2개 담았을 때 3이 아니라 2가 돼요. 충돌하면 새 값으로 덮어쓰거든요. “있으면 더하기”는 DB 함수 안에서 ON CONFLICT … DO UPDATE SET quantity = quantity + …로만 돼요. 동사를 “담는다”가 아니라 “있으면 더한다”로 적어야 이 차이가 설계 단계에서 보여요.

3. 담기는 재고를 줄이지 않는다. 그래서 두 사람이 마지막 재고 하나를 동시에 담을 수 있어요. 문제는 주문 단계에서 터지고, 거기서 재고 열의 CHECK가 둘 중 하나를 막아요. 10월 19일 확인에서는 동시 실행을 재현하지 않았고 논리로만 확인했다고 적어 두었어요. 이 글의 PGlite 실험도 단일 사용자 순차 실행이에요.

4. 없는 상품은 404 화면인데 상태 코드는 200이다. 10월 19일 기록에 따르면 notFound()가 스트리밍 중에 불리면 응답 머리는 이미 나간 뒤라 화면은 404, HTTP(웹의 요청·응답 규약) 상태 코드는 200이었어요. 여정의 “결정” 단계에서 읽기 동사가 “없음”을 돌려줄 때 화면과 상태 코드를 따로 확인해야 해요.

5. 로그인 방식이 둘 공존한다. 팀원의 로그인은 아이디와 JSON 파일과 자체 세션 쿠키이고, 10월 19일에 제가 붙인 가입은 Supabase Auth(Supabase가 제공하는 인증 기능)예요. 장바구니와 주문의 정책이 auth.uid()(로그인한 사용자의 id를 돌려주는 DB 함수)를 쓰니, 팀원 로그인으로는 담기가 “로그인 필요”로 나와요. 회원 도메인이 “요청자가 누구인가”를 소유한다는 말은, 그 답이 하나여야 한다는 뜻이기도 해요. 팀 결정이 필요한 상태입니다.

요약

백엔드 설계는 테이블이 아니라 손님의 여정에서 시작한다. 단계마다 화면이 있고, 화면마다 저장할 명사와 바꿀 동사가 적혀 있다.

도메인은 바뀌는 이유가 하나인 업무의 묶음이고, 단계는 시간순이지만 도메인은 겹쳐 쓰인다. 주문 한 단계가 세 도메인을 건드린다.

쓰기 동사가 둘 이상 모인 단계에만 경계를 긋는다. 이 쇼핑몰에서는 주문 하나이고, 경계가 없으면 유령 주문이 남는다.

읽기는 서버 컴포넌트가, 쓰기는 서버 액션이 함수 하나를 불러서 한다. 행을 지키는 것은 키가 아니라 DB의 정책이다.

다음 글은 ②-2 ERD예요. 이 글이 넘긴 여덟 테이블 목록을 받아 표기법과 관계선을 읽고, 키와 N:M과 정규화와 스냅샷을 정하고, 1차에서 3차까지 ERD가 어떻게 바뀌었는지와 DDL(테이블을 실제로 만드는 SQL 문)로 검증한 기록을 다룹니다. 3편은 그 데이터 구조에 맞춘 프론트엔드, 4편은 프론트와 백엔드를 실제로 잇는 이야기예요.

출처 — 개념은 데이터베이스 수업(데이터 모델링 절차, 트랜잭션과 원자성, 행 단위 보안, 쓰기 경로 단일화, 주문 도메인 설계 실습)과 프론트엔드 수업(도메인 폴더 구조, 서버 액션, 두 연결 객체)에서 배운 것을 제 말로 다시 썼다. 고객 여정 · 퍼널 분석 · 서비스 블루프린트는 마케팅 용어집과 제 정리에서 가져왔고, 그중 블루프린트는 전용 출처 없는 일반 지식 서술이다(위키 신뢰도 low). 도메인 폴더 구조와 서버 액션 · 연결 객체 페이지도 단일 코스 · 단일 출처 기준이라 low 이고, 10-19 로컬 검증이 1차 확인이다. 팀의 ERD 2차 · 3차 설계와 10-19 연결 계획은 AI 와 함께 정리한 제안이고 튜터 검토는 받지 않았다. 증거는 2026-10-15 · 16 · 19 작업일지와 10-16 회고, 2026-10-19 의 Supabase 연결 확인 기록, 그리고 2026-10-20 에 PGlite 0.5.10 (PostgreSQL 18.6) 으로 직접 돌린 트랜잭션 실험과 팀 저장소 복사본에서 뽑은 폴더 트리 · 화면 캡처다. 저장소 이름과 팀원 정보는 적지 않았다.

관련 키워드

고객 여정도메인 설계트랜잭션서버 액션Row Level SecuritySupabase쇼핑몰 클론팀 프로젝트

앤디의 마케팅노트

마케팅을 시스템으로 만드는 과정을 매주 기록해요. 잘 된 것도, 안 된 것도 같이 봐요.

maily에서 구독하기