- 작성일
Stairno 2. 계획서와 다른 도메인 로그인 실험
- Authors

- Name
- seren-wib
Contents
로드맵 2단계. 계정 센터 코드를 한 줄도 쓰기 전에 한 일이다. 전체 그림은 Stairno 0. 전체 로드맵에 있다.
한 줄 요약
계정 센터가 무엇을 하고, 무엇을 안 하고, 어떻게 만들지를 문서 네 개로 먼저 정했다. 그리고 가장 불확실했던 질문, "다른 도메인 사이트끼리 로그인을 공유할 수 있나"를 작은 실험으로 확인했다. 답은 "쿠키로는 안 되고, OIDC로는 된다"였다.
1. 계정 센터가 별도 프로젝트가 된 날
10월 17일, Labs(제품 스토어) 계획서를 쓰다가 문제가 보였다. Labs는 로그인과 결제가 필요하다. Works(외주)도 선금 결제가 필요하다. 나중에 나올 제품들도 로그인이 필요하다. 이걸 각 사이트 계획서에 하나씩 넣으면, Stairno 0에서 말한 "같은 일 네 번"이 그대로 일어난다.
그래서 Labs 계획서의 결정 기록에 이렇게 적고, 계정과 결제를 별도 프로젝트, 별도 레포로 떼어 냈다.
- 계정 센터
account.stairno.com과 공용 APIapi.stairno.com을 하나의 프로젝트로 만든다 - 허브를 뺀 모든 Stairno 사이트와 제품은 자기 계정과 결제를 두지 않고 이 둘을 부른다
- 국내 결제는 토스페이먼츠, 해외 결제는 PayPal
같은 날 밤 빈 레포 seren-wib/account.stairno.com을 만들었다. README 한 줄짜리였다.
2. 왜 문서부터 쓰나
로그인과 결제는 실수하면 남의 계정이 털리거나 남의 돈이 잘못 나가는 영역이다. 그리고 나는 의뢰인이자 개발자다. 혼자 하면 "어디까지 만들기로 했더라"가 머릿속에만 있어서, 하다 보면 범위가 계속 늘어난다.
그래서 학교 소프트웨어공학 수업에서 배운 산출물을 그대로 썼다. 소개 사이트였던 허브도 문서를 썼지만, 계정 센터는 로그인과 결제를 다루니까 줄이지 않고 풀 세트로 썼다.
문서 네 개
| 문서 | 질문 | 들어 있는 것 |
|---|---|---|
| 프로젝트 계획서 | 왜, 어디까지, 무엇이 위험한가 | 목표, 범위(1차 로그인 / 2차 결제 / 3차 확장 / 범위 밖), 위험과 대응, 비용, 결정 기록 |
| 요구사항 명세서(SRS) | 무엇을 만드나 | 기능 요구사항 85개, 비기능 21개, 도메인 9개, 사용 시나리오 12개 |
| 설계 명세서(SDD) | 어떻게 만드나 | 구조, 폴더, DB 표, API, 결제 순서도, 화면, 오류 코드, 결정 기록 |
| 테스트와 배포 문서 | 어떻게 확인하고 올리나 | 테스트 층, 배포 절차, 리허설 절차, 인수 체크리스트, 개인정보 유출 사고 대응 |
원본은 마크다운으로 쓰고, 공유용으로 Google Docs에 옮겨 둔다. 코드와 문서가 어긋나면 같은 변경 안에서 둘 중 하나를 고친다는 규칙도 이때 정했다.
계획서: "안 하는 것"을 먼저 적는다
계획서에서 제일 쓸모 있었던 건 범위 밖 목록이다.
- 소셜 로그인(구글, 애플), 패스키, 2단계 인증 → 3차. 1차는 이메일과 비밀번호만
- 관리자 화면 → 안 만든다. 초기엔 DB와 결제사 대시보드를 직접 본다
- 제품 카탈로그, 체크아웃 화면 → Labs의 일
- 실제 결제 → 사업자등록 뒤. 그 전엔 전부 테스트 모드
그리고 "범위를 늘리는 변경은 계획서의 범위와 결정 기록을 먼저 고친 뒤에만 한다"는 규칙을 넣었다. 하다가 "이것도 넣자"가 생기면 문서부터 고쳐야 하니, 즉흥적으로 범위가 늘지 않는다.
위험 표도 미리 적었다. 예를 들면 이런 것들이다.
| 위험 | 대응 |
|---|---|
| 다른 도메인끼리 로그인이 공유되지 않는다 | 설계 전에 실험으로 확인한다(아래 4절) |
| 고른 도구들이 Cloudflare Workers에서 안 돈다 | 코드 첫날에 가입·로그인을 실제로 배포해 본다. 안 되면 런타임을 바꾼다 |
| 결제 사고(이중 결제, 알림 유실, 금액 불일치) | 주문 상태 규칙을 먼저 그린다. 서버가 금액을 다시 확인한다 |
| 카드 정보 취급 | 카드 번호는 받지도 저장하지도 않는다. 결제사 화면이 받는다 |
| 혼자라서 중간에 멈춘다 | 1차만으로도 사이트 로그인이 되게 끝낸다 |
SRS: 번호가 붙은 약속
요구사항 하나하나에 번호를 붙인다. 예를 들어 이런 식이다.
FR-3 이메일과 비밀번호로 로그인한다. 실패 사유는 "이메일 또는 비밀번호가 맞지 않습니다" 하나로만 알린다. 확인 방법: 없는 이메일과 틀린 비밀번호의 응답 본문이 같다.
왜 실패 이유를 하나로 뭉치나? "그런 이메일은 없습니다"와 "비밀번호가 틀렸습니다"를 구분해서 알려 주면, 공격자가 아무 이메일이나 넣어 보면서 "이 사람이 Stairno에 가입했는지"를 알아낼 수 있다.
이 번호는 테스트 이름에도 그대로 들어간다(FR-3 returns the same body for an unknown email and a wrong password). 나중에 인수 체크리스트에서 "FR-3 확인했나?"가 테스트 이름 검색 한 번이 된다.
레포 문서
코드 레포 안에는 세 문서를 따로 둔다.
README.md: 이 레포가 뭔지, 어떻게 돌려 보는지. 공개 문서라 영어로 쓴다CONTRIBUTING.md: 브랜치, 커밋, PR, 배포 규칙CLAUDE.md: AI 코딩 에이전트가 읽는 작업 규칙. "DB는 API만 만진다", "카드 번호를 받는 코드를 만들지 않는다", "시크릿은 레포에 쓰지 않는다" 같은 것들
3. 가장 큰 질문: 다른 도메인끼리 로그인이 되나
계정 센터의 존재 이유는 "account.stairno.com에서 로그인하면 stairnolabs.com에서도 로그인 상태"다. 이게 안 되면 나머지 설계가 전부 의미가 없다. 그래서 문서를 쓰는 중간에 실험부터 했다.
쿠키로는 왜 안 되나
로그인 상태는 보통 쿠키로 기억한다. 그런데 쿠키는 그걸 준 사이트에만 보내진다.
account.stairno.com과api.stairno.com은 같은 집(stairno.com)의 방이라 설정하면 쿠키를 같이 쓸 수 있다stairnolabs.com은 다른 집이다.stairno.com의 쿠키가 거기로 가지 않는다- 다른 집 쿠키를 빌려 쓰는 서드파티 쿠키는 Safari 같은 브라우저가 막는다
그러니 쿠키를 공유하는 길은 처음부터 없다.
대안: OIDC
"구글로 로그인"과 같은 방식인 OIDC를 쓰면 된다. 계정 센터가 신분증 발급처(공급자)가 되고, 각 사이트는 신분증을 받아 가는 쪽(클라이언트)이 된다. 사이트는 사용자를 계정 센터로 보냈다가, 일회용 코드를 들고 돌아온 사용자를 받아 자기 쿠키로 자기 로그인을 만든다. 쿠키는 각 집에 머물고, 집끼리는 리다이렉트로만 만난다.
문제는 내가 고른 인증 부품 Better Auth가 OIDC 공급자 역할까지 제대로 해 주느냐였다. 그 기능(@better-auth/oauth-provider)은 비교적 새로 나온 부분이라 문서만 믿을 수 없었다.
실험 방법
- 내 컴퓨터에 서버 둘을 띄운다. 하나는 계정 센터 대역(포트 4101), 하나는 Labs 대역(포트 4102). 둘 다 Better Auth를 쓴다
- 계정 센터 대역에는 OIDC 공급자 기능을 켜고, Labs 대역은 그 공급자를 가리키는 클라이언트로 설정한다
- 브라우저 대신 스크립트가 움직인다. 스크립트는 출처마다 쿠키 통을 따로 들고, 진짜 브라우저처럼 리다이렉트를 하나씩 따라간다. 그래야 "쿠키가 섞이지 않는 상태"에서 로그인이 되는지 볼 수 있다
실험 결과: 13단계 전부 통과
| 단계 | 한 일 | 결과 |
|---|---|---|
| 1 | 계정 센터에 가입 | 계정 센터 쿠키 생김 |
| 2 | Labs를 계정 센터에 사이트로 등록 | 사이트 이름표(client_id)와 비밀번호(client_secret) 발급 |
| 3 | 계정 센터의 안내문 읽기 | 로그인 주소, 토큰 주소, 공개 열쇠 주소가 다 있음 |
| 4 | Labs에서 "Stairno로 로그인" | 계정 센터 주소로 보내짐. PKCE 적용됨 |
| 5 | 계정 센터에 로그인 안 한 상태로 도착 | 로그인 페이지로 감. 원래 요청 정보는 위조 방지 서명을 붙여 따라옴 |
| 6 | 로그인 페이지에서 비밀번호 입력 | 로그인 응답이 바로 "Labs로 돌아가라 + 코드" |
| 7 | 이미 로그인한 상태로 다시 요청 | 동의 화면 없이 리다이렉트 한 번에 Labs로 |
| 8 | Labs가 코드를 토큰으로 바꿈 | 성공. Labs가 자기 쿠키로 자기 로그인을 만듦 |
| 9 | Labs에 "지금 누구야?" | 계정 센터와 같은 사람(이메일 같음) |
| 10 | 쿠키 바꿔 끼워 보기 | 계정 센터 쿠키로 Labs 조회 → 아무도 아님. 반대도 마찬가지. 로그인은 사이트별로 따로 |
| 11 | Labs가 받은 액세스 토큰 검사 | JWT. 계정 센터의 공개 열쇠로 서명 확인 통과, 유효기간 1시간 |
| 12 | 신원 토큰, 갱신 토큰 | 둘 다 있음 |
| 13 | 계정 센터 로그인이 살아 있을 때 Labs에서 다시 로그인 | 비밀번호 없이 리다이렉트 한 번 |
결론: 된다. 쿠키 공유가 아니라 OIDC다.
실험에서 배운 것
실험을 안 했으면 코드를 짜다가 하나씩 부딪혔을 것들이다. 전부 설계 명세서에 옮겨 적었다.
- 액세스 토큰은 기본으로 "아무 뜻 없는 문자열"로 나온다. 요청할 때 "이 토큰은
api.stairno.com용"이라고 밝혀야(resource값) 내용을 확인할 수 있는 JWT로 나온다. API는 그 "누구용인지" 값을 검사한다 - 실제 운영용 사이트는 돌아올 주소가 https여야 한다. 실험은 로컬이라 예외 설정으로 했다
- 사이트끼리는 다 Stairno라서 "이 사이트가 내 정보를 받아도 됩니까?" 동의 화면을 건너뛰게 등록한다. 이 설정은 서버 안쪽의 관리 명령으로만 할 수 있게 하고, 아무나 사이트를 등록하는 기능은 끈다
- 로그인 페이지는 따라온 요청 정보를 그대로 들고 있다가 비밀번호와 함께 보내야 한다. 그래야 로그인이 끝나자마자 원래 사이트로 돌아간다
- 실험에서 못 본 것: 진짜 https 환경,
stairno.com서브도메인 사이의 쿠키 공유, Cloudflare Workers에서 돌아가는지. 이건 3단계 첫날 운영에서 확인하기로 했다
실험 코드는 레포의 experiments/cross-domain-login에 남겨 두고, Better Auth 버전을 올릴 때마다 다시 돌려서 13개가 그대로 통과하는지 본다.
4. 구조: 레포 하나, 서버 둘
1차(로그인) 기준 구조. 2차에서 API 뒤에 결제사와 파일 저장소가 더해진다
api.stairno.com(공용 API): 로그인, 세션, OIDC 공급자, 나중엔 주문과 결제까지. DB를 만지는 건 여기뿐이다account.stairno.com(계정 센터): 사람이 보는 화면. DB를 직접 열지 않고 API를 부른다. 서버에서 화면을 만들 때도 브라우저가 보낸 쿠키를 그대로 API에 넘겨 "이 사람 누구야?"를 묻는다- 사이트들: OIDC로 로그인하고, 토큰을 들고 API를 부른다
"DB는 API만 만진다"는 규칙 하나 덕분에, 데이터 규칙(누가 무엇을 볼 수 있나)이 API 한 곳에만 있다. 화면이 몇 개로 늘어나도 데이터 규칙은 한 벌이다.
5. 도구를 고른 이유
기준은 Stairno 0의 첫 원칙, 혼자 운영할 수 있는가다.
| 영역 | 고른 것 | 이유 |
|---|---|---|
| 서버 | Cloudflare Workers | 허브와 같은 곳. 서버 컴퓨터 관리 없음, 무료로 시작, Cron으로 구독 갱신 가능 |
| API 뼈대 | Hono | 어느 서버 환경에서든 돈다. Workers가 안 맞으면 다른 곳으로 옮겨도 코드 대부분이 그대로 |
| 인증 | Better Auth + OIDC 공급자 + JWT | 가입, 로그인, 세션, 속도 제한이 들어 있고, 실험으로 OIDC 공급자 역할을 확인함 |
| DB | Neon(PostgreSQL) + Drizzle | 남이 관리해 주는 DB, 무료 등급. Workers에서 도는 연결 방식이 있음. 표 설계가 코드로 남음 |
| 메일 | Resend | 보내는 방법이 단순하고 월 3,000통 무료. 도메인 인증으로 스팸함에 안 가게 |
| 화면 | Astro + @stairno/ui | 허브와 같은 도구, 같은 모양. 로그인한 사람마다 화면이 달라서 SSR |
| 테스트 | Vitest | 부품 하나부터 실제 서버 엔진까지 같은 도구로 |
테스트 모드 동안 고정비는 0원이다. 실제 결제가 시작되면 Workers 유료 플랜(월 5달러)부터 든다.
6. 2단계 끝
10월 18일 새벽, 문서 넷을 마치고 레포 문서와 실험 코드를 레포에 넣으면서 2단계를 닫았다. 다음은 이 문서대로 실제로 만드는 3단계다.
다음 글: Stairno 3. 로그인 만들고 공개하기