SaaS를 처음부터 만들 때 가장 먼저 막히는 지점은 화면이 아니다. "로그인한 사람이 누구인지"와 "이 사람이 돈을 냈는지"를 같은 코드베이스 안에서 어떻게 이어 붙이느냐다. 이 둘을 대충 연결하면 나중에 결제 로직을 건드릴 때마다 인증 코드까지 같이 흔들린다. 인증·사용자·결제를 하나의 파이프라인처럼 순서대로 쌓는 리액트 라우터 기반 SaaS 스타터 킷 구조를 뜯어보면서, 왜 이 순서가 재작업을 줄이는지 정리한다.

왜 순서가 구조를 결정하는가

집을 지을 때 지붕부터 올리는 사람은 없다. 기초를 다지고 기둥을 세운 다음에야 지붕을 얹는다. SaaS 코드베이스도 똑같다. 인증(누구인지 확인하는 절차)이 기초고, 사용자 프로필(그 사람의 정보를 담는 테이블)이 기둥이고, 결제는 지붕이다. 순서를 바꾸면 나중에 기둥을 다시 세우려고 지붕을 뜯어야 한다.

많은 프로젝트가 반대로 간다. 결제 붙이기가 급하니 결제 연동부터 하고, 인증은 "나중에 정리하지"라며 미룬다. 그 결과 결제 성공 콜백 안에 사용자 식별 로직이 끼어 들어간다. 인증 방식을 하나 추가하려면 결제 코드까지 손대야 하는 상황이 온다. 순서를 지키지 않은 대가는 몇 주 뒤가 아니라 다음 스프린트에 바로 청구서로 날아온다.

구조를 계층으로 나누면 이 문제가 사라진다. 인증 계층은 "이 요청을 보낸 게 누구인가"만 답한다. 사용자 계층은 "이 사람의 프로필과 설정이 무엇인가"만 답한다. 결제 계층이 답하는 질문은 딱 하나, "이 사람이 지불할 자격이 있는가"뿐이다. 각 계층은 자기 질문에만 집중하고, 아래 계층의 답을 그대로 신뢰한다.

인증을 가장 먼저 굳혀야 하는 이유

인증은 로그인, 회원가입, 비밀번호 재설정, 소셜 로그인 같은 "본인 확인" 절차 전체를 말한다. 이걸 가장 먼저 굳히는 이유는 단순하다. 위에 쌓이는 모든 기능이 "로그인한 사용자"라는 전제를 깔고 시작하기 때문이다. 전제가 흔들리면 그 위에 쌓은 것도 다 흔들린다.

실무에서 자주 놓치는 부분은 세션이 바뀌는 순간이다. 비밀번호를 바꾸거나 로그아웃을 하면 인증 서버가 새 응답 헤더(브라우저에 세션 정보를 전달하는 값)를 내려준다. 이 헤더를 중간에 놓치면 사용자는 로그아웃했는데도 이전 세션이 살아있는 것처럼 동작한다. 반대로 정상 로그인인데 세션이 끊긴 것처럼 보이기도 한다. 인증 흐름을 처음 설계할 때 이 헤더 전달을 빠짐없이 챙기는 습관이 나중 디버깅 시간을 크게 줄인다.

매직링크나 OTP(문자·이메일로 받는 일회용 인증 코드), 소셜 로그인처럼 여러 인증 수단을 동시에 지원하려면 진입점은 여러 개라도 결과는 하나로 모아야 한다. 어떤 방식으로 들어오든 결국 같은 형태의 세션이 만들어지게끔 설계하면, 나중에 인증 수단을 하나 더 추가해도 위 계층은 건드릴 필요가 없다.

사용자 프로필은 인증과 분리된 별도 테이블이다

여기서 많이 하는 실수가 하나 있다. 인증 시스템이 제공하는 사용자 테이블에 프로필 정보까지 욱여넣는 것이다. 이름, 아바타, 알림 설정 같은 필드를 인증 테이블에 붙이면 당장은 편하다. 문제는 인증 공급자를 바꾸거나 다중 인증을 추가할 때 터진다. 프로필 데이터가 인증 시스템에 종속돼 있으면 마이그레이션 자체가 재작성 수준의 작업이 된다.

그래서 인증 테이블과 프로필 테이블을 처음부터 나누는 편이 낫다. 인증 테이블은 "이 사람이 누구인지 증명하는 값"만 갖고, 프로필 테이블은 "이 사람에 대한 부가 정보"만 갖는다. 둘을 사용자 ID 하나로 연결한다. 아바타 업로드, 이메일 변경, 소셜 계정 연결·해제 같은 기능은 전부 프로필 계층에서 처리하고, 인증 계층은 "그 변경이 실제 본인인지"만 확인한다.

계정 삭제처럼 되돌릴 수 없는 작업은 이 분리 구조 덕분에 오히려 더 안전해진다. 프로필 데이터를 지우는 절차와 인증 정보를 지우는 절차를 따로 감사(누가 언제 무엇을 지웠는지 기록)할 수 있기 때문이다. 두 절차가 한 덩어리였다면 실수로 하나만 지우고 하나는 남는 사고가 생기기 쉽다.

개발자가 노트북 화면에서 인증과 사용자 프로필 테이블 구조를 검토하는 모습
인증 테이블과 프로필 테이블을 분리해 설계하는 작업 장면.

결제는 왜 맨 마지막에 얹는 지붕인가

결제 로직은 인증과 프로필이 이미 안정된 다음에 붙여야 한다. 이유는 결제 성공 여부를 판단하려면 "이 요청을 보낸 사용자가 누구인지"가 이미 확정돼 있어야 하기 때문이다. 인증이 흔들리는 상태에서 결제를 붙이면, 결제 승인 로직 안에 "혹시 몰라서" 넣은 사용자 확인 코드가 중복으로 쌓인다.

결제 연동에서 흔히 보는 데모 수준의 설계는 결제 금액을 코드에 고정값으로 박아 두는 것이다. 개발 단계에서는 편하지만 실제 서비스에서는 위험하다. 클라이언트가 보낸 금액을 그대로 믿으면 안 되고, 서버가 주문을 생성할 때 정한 금액과 결제 승인 시점의 금액이 일치하는지 서버 쪽에서 다시 확인해야 한다. 결제 승인 API를 호출할 때도 클라이언트가 아니라 서버가 보관한 비밀 키로 검증하는 구조가 기본이어야 한다.

중복 결제 방지도 놓치기 쉬운 부분이다. 결제 성공 콜백이 네트워크 문제로 두 번 호출되는 경우가 실제로 발생한다. 이때 같은 주문 번호로 두 번째 삽입을 막는 멱등성(같은 요청을 여러 번 보내도 결과가 한 번만 반영되는 성질) 처리가 없으면 사용자는 한 번 결제했는데 이력에는 두 번 찍힌다. 결제 이력 테이블에 주문 번호를 고유 값으로 걸어 두는 정도의 방어만으로도 이 사고 대부분을 막을 수 있다.

데모와 실서비스 사이, 자주 놓치는 지점

스타터 킷이 제공하는 결제·인증 예제는 대개 "동작한다"를 보여주는 데 초점이 있지, "안전하다"를 보장하지는 않는다. 이 둘을 같다고 착각하면 실서비스에서 사고가 난다. 아래는 실제로 자주 걸리는 지점을 정리한 것이다.

영역 데모 수준 설계 실서비스 전환 시 확인할 것
결제 금액 클라이언트가 보낸 금액을 그대로 사용 서버가 생성한 주문 금액과 승인 시점 금액을 서버에서 대조
결제 중복 콜백이 한 번만 온다고 가정 주문 번호에 고유 제약을 걸어 중복 삽입 차단
세션 헤더 일부 응답에서 세션 헤더 갱신 누락 비밀번호 변경·로그아웃 등 세션이 바뀌는 모든 지점에서 헤더 전달 확인
계정 삭제 관리자 권한으로 즉시 삭제 삭제 절차를 감사 로그로 남기고 되돌릴 여지를 검토
디버그 라우트 개발 편의용 경로가 그대로 배포됨 배포 전 디버그 경로 제거 또는 접근 차단
인증 계층, 사용자 프로필 계층, 결제 계층이 아래에서 위로 쌓이며 각 계층이 아래 계층을 신뢰하는 구조를 보여주는 다이어그램
인증-사용자-결제가 순서대로 쌓이는 계층 구조 다이어그램.

배포 전 최소 점검 체크리스트

새 SaaS 프로젝트를 실서비스로 넘기기 전에, 아래 항목만이라도 순서대로 확인하면 흔한 사고 대부분을 피할 수 있다.

  • 로그인·회원가입·비밀번호 재설정·소셜 로그인 전 경로에서 세션 헤더가 정상 전달되는지 확인했다.
  • 사용자 프로필 테이블이 인증 테이블과 분리돼 있고, 사용자 ID로만 연결돼 있다.
  • 결제 금액을 서버에서 생성하고, 승인 시점에 서버가 그 금액과 대조한다.
  • 결제 이력 테이블에 주문 번호 고유 제약이 걸려 있어 중복 삽입이 불가능하다.
  • 계정 삭제, 결제 환불 같은 되돌릴 수 없는 작업에 감사 로그가 남는다.
  • 디버그·테스트용 라우트를 배포 전 제거하거나 접근을 막았다.

세 계층을 하나로 묶을 때 지키는 원칙

인증·사용자·결제를 각각 잘 만들어도 이어 붙이는 지점에서 다시 무너지는 경우가 있다. 원칙은 하나다. 위 계층은 아래 계층의 결과만 신뢰하고, 아래 계층의 내부 구현은 모른 채로 둔다. 결제 계층은 "이 사용자 ID가 유효한 세션에서 왔다"는 사실만 알면 되고, 그 세션이 이메일 로그인인지 소셜 로그인인지는 알 필요가 없다.

라우팅 구조도 이 원칙을 그대로 반영하면 좋다. 로그인하지 않은 사용자만 볼 수 있는 공개 페이지와, 로그인한 사용자만 볼 수 있는 비공개 페이지를 레이아웃 단계에서 갈라놓으면 각 라우트 안에서 매번 "로그인했는지" 검사하는 코드를 반복하지 않아도 된다. 결제 관련 라우트는 비공개 레이아웃 안에 자연스럽게 들어가고, 결제 로직은 이미 확인된 사용자를 전제로 짤 수 있다.

API 형태로 노출되는 경로와 화면을 그리는 경로도 구분해 두면 결제 콜백이나 웹훅(외부 서비스가 이벤트를 알려주는 요청)을 처리할 때 화면 렌더링 로직과 뒤섞이지 않는다. 결제 승인 콜백은 화면이 필요 없다. 데이터를 받아 검증하고 저장하는 일만 하면 된다. 이 구분이 없으면 결제 콜백 코드 안에 렌더링 코드가 섞여 들어가고, 둘 중 하나를 고칠 때 나머지를 깨뜨릴 위험이 커진다.

공개 라우트와 비공개 라우트를 레이아웃 단계에서 분리하고 결제 콜백을 화면 렌더링과 별도 경로로 처리하는 라우팅 구조 다이어그램
공개·비공개 라우트 분리와 결제 콜백 경로를 나타낸 다이어그램.

순서를 지키면 재작업이 아니라 확장이 된다

인증을 먼저 굳히고, 사용자 프로필을 분리하고, 결제를 마지막에 얹는 순서는 어렵지 않다. 다만 급할 때는 자꾸 건너뛰고 싶어진다. 그 유혹을 넘긴 프로젝트와 넘기지 못한 프로젝트의 차이는 몇 달 뒤 기능 하나를 추가할 때 드러난다. 계층이 분리돼 있으면 새 인증 수단이나 새 결제 수단을 추가할 때 그 계층만 건드리면 끝난다. 분리가 안 돼 있으면 추가가 아니라 재작업이 된다.

SaaS 스타터 킷을 고를 때도 화면이 예쁜지보다 이 계층 분리가 얼마나 철저한지를 먼저 본다. 동시성 문제처럼 코드베이스 어디서든 터질 수 있는 이슈를 미리 정리해 두는 습관과 마찬가지로, 인증·사용자·결제 순서를 처음부터 지키는 습관이 장기적으로 훨씬 적은 재작업을 만든다. 관련해서 동시성 버그를 다룬 동시성 문제 3가지 글도 함께 보면 도움이 된다. 이런 아키텍처 이야기는 프로그래밍 카테고리에서 더 볼 수 있다.