logo
작성일

Stairno 3. 로그인 만들고 공개하기

Authors
  • avatar
    Name
    seren-wib
    Twitter
Contents

로드맵 3단계. 2단계에서 쓴 문서대로 로그인을 실제로 만들어 공개했다. 전체 그림은 Stairno 0. 전체 로드맵에 있다.

한 줄 요약

10월 18일 새벽부터 오전까지, 빈 레포에 가입·로그인·인증 메일·기기 관리·다른 사이트용 로그인 창구를 만들고 api.stairno.com과 account.stairno.com을 공개했다. 오후에는 허브와 같이 쓰는 계정 메뉴를 붙이고, 운영 주소에서 인수 체크리스트를 확인해 3단계를 닫았다.

1. 일하는 방식

코드를 쓰기 전에 규칙부터 정했다.

  • 기능 하나 = 이슈 하나 + PR 하나. "비밀번호 재설정 메일" 같은 기능 하나를 카드로 적고, 그 기능만 담은 변경을 올린다
  • 커밋은 test → feat 한 쌍. TDD대로 실패하는 테스트를 먼저 커밋하고, 그걸 통과시키는 코드를 다음 커밋으로 올린다. 기록만 봐도 "무엇을 약속했고, 어떻게 지켰나"가 보인다
  • 테스트 이름에 요구사항 번호. FR-3 returns the same body for an unknown email and a wrong password처럼
  • main에 머지되면 바로 배포. 이 단계에서는 PR이 CI를 통과하면 main에 넣고, main은 자동으로 운영에 올라갔다. 이 방식은 4단계에서 바꾼다

코드는 AI 코딩 에이전트가 이 규칙대로 썼고, 나는 PR을 보고 결정을 내렸다. 3단계 동안 main에 들어간 PR이 60개가 넘는다.

2. 뼈대 세우기

레포 하나 안에 세 덩어리를 둔다.

폴더무엇
apps/api공용 API. Hono + Better Auth + DB
apps/account계정 센터 화면. Astro
packages/contract둘이 같이 쓰는 약속. 입력 형식 검사 규칙, 오류 코드 목록

packages/contract가 있는 이유: 가입 폼에서 "비밀번호는 몇 자 이상"을 화면이 검사하고, API도 똑같이 검사해야 한다. 규칙을 두 군데 따로 적으면 언젠가 어긋난다. 그래서 한 곳에 적고 둘이 가져다 쓴다.

CI는 PR마다 이것들을 돌린다.

  1. 코드 서식 검사
  2. 타입 검사(값의 종류가 맞는지)
  3. 테스트 전부
  4. DB 설계도와 마이그레이션 파일이 서로 맞는지
  5. 두 서버를 실제로 빌드해 보기
  6. 실제 Workers 엔진(workerd) 위에서 가입 → 로그인을 처음부터 끝까지

3. 첫날 확인: 정말 Workers에서 도나

2단계 위험 표의 두 번째 줄, "고른 도구들이 Workers에서 안 돈다"를 첫날 확인했다. Better Auth, Drizzle, Neon 연결 셋 다 문서로는 Workers를 지원한다고 했지만, 실제로 돌려 보기 전엔 모른다.

결론은 "된다". 대신 Node.js에서는 안 보이던 버그가 하나 나왔다.

서버가 깨어난 직후(콜드 스타트) 첫 요청이 상태 확인(/health)이면, 그 뒤의 로그인 요청이 전부 응답 없이 멈췄다.

원인은 이렇다. 첫 요청이 Better Auth를 준비시키기 시작해 놓고, 준비가 끝나기 전에 자기 일을 마치고 끝나 버린다. workerd는 요청이 끝나면 그 요청이 벌여 놓은 일을 버린다. 그래서 "준비 중" 상태가 영원히 끝나지 않고, 그걸 기다리는 다음 요청들이 같이 멈춘 것이다. Node.js는 벌여 놓은 일을 버리지 않아서 이 문제가 안 보인다.

고친 방법: 준비를 시작한 요청이 준비가 끝날 때까지 기다렸다가 끝나게 했다. "첫날 Workers에서 실제로 해 본다"를 위험 대응으로 적어 둔 게 맞았다.

4. 기능을 하나씩

순서대로 하나씩, 매번 테스트 먼저.

API 쪽

기능내용
가입이메일, 비밀번호, 약관 동의. 유출된 적 있는 비밀번호는 거절
인증 메일가입하면 "이메일 확인" 메일. 한국어와 영어, 사용자 언어로
로그인실패 사유는 하나로만(없는 이메일인지 틀린 비밀번호인지 안 알려 준다)
로그아웃, 모든 기기 로그아웃내 세션 하나, 또는 전부를 지운다
비밀번호 재설정메일로 받은 링크에서 새 비밀번호
프로필이름, 언어, 이메일 변경
기기 목록어디서 로그인돼 있는지 보고, 원하는 기기만 로그아웃
계정 삭제비밀번호를 한 번 더 확인한 뒤 삭제
세션 규칙30일 유지, 매일 쓰면 하루씩 연장. 쿠키는 자바스크립트가 못 읽고 https로만 오가게
사이트 로그인 창구OIDC 공급자. 사이트 등록은 운영자 명령으로만
토큰액세스 토큰 1시간, 리프레시 토큰 90일
연결된 사이트어떤 사이트에 Stairno로 로그인했는지 보고 연결 끊기
오류 형식모든 오류를 { error: { code, message } } 한 모양으로. 코드는 정해 둔 목록에서만
보안등록된 사이트만 API를 부를 수 있게(CORS), 보안 헤더, 로그인·가입·재설정·메일 재전송 속도 제한
메일 기록보낸 메일을 기록하고, 메일 서비스가 실패해도 가입 자체는 깨지지 않게

화면 쪽

로그인, 가입, 비밀번호 재설정, 이메일 확인, 동의 화면, 계정 첫 화면, 프로필, 로그인 기기, 연결된 사이트, 계정 삭제. 주소 /로 오면 브라우저 언어를 보고 /ko나 /en으로 보낸다.

화면에는 **콘텐츠 보안 정책(CSP)**도 걸었다. "이 페이지에서는 이 주소의 스크립트만 실행된다"는 허용 목록이다. 누가 페이지에 몰래 스크립트를 끼워 넣어도 브라우저가 실행하지 않는다.

약관과 개인정보처리방침

계정 정보를 받는 곳이니 계정 센터에 둔다. 개인정보보호법의 해당 조항과 개인정보보호위원회의 작성 지침에 있는 필수 항목을 체크리스트로 만들어 하나씩 채웠다. 받는 정보는 최소로(이메일, 이름, 결제 기록), 맡기는 곳(Cloudflare, Neon, Resend)과 그 나라를 적었다. 가입할 때 만 14세 이상 확인도 넣었다.

처음엔 운영자 칸에 내 이름을 넣었다가, 공개 페이지에 개인 이름이 박히는 게 걸려서 "Stairno 개인정보 보호 담당"과 문의 메일로 바꿨다. 실제 결제를 시작하는 9단계(사업자등록)에서는 법에 따라 대표자 이름을 다시 넣는다.

5. 공개하기

배포 자동화

main에 머지되면 GitHub Actions가 이 순서로 운영에 올린다.

  1. 테스트
  2. 운영 DB에 마이그레이션 적용
  3. API 배포
  4. 계정 센터 배포

DB 구조를 바꾸는 단계가 실패하면 그 뒤를 아예 하지 않는다. 새 코드가 옛 DB 구조 위에서 도는 상황을 막으려는 것이다.

사람이 한 일

자동화 전에 한 번은 사람이 해야 하는 일이 있었다.

  • Neon에 DB 만들기, 첫 마이그레이션은 손으로
  • Cloudflare에 두 서버의 시크릿 넣기, GitHub에 배포용 열쇠 넣기
  • 서브도메인 api, account를 서버에 연결

10월 18일 오전 11시쯤 api.stairno.com과 account.stairno.com이 운영에 올라갔다.

메일이 스팸함에 안 가게

인증 메일이 스팸함에 가면 가입이 막힌다. 그래서 Resend에 stairno.com 도메인을 인증했다. 메일 받는 쪽이 "이 메일이 정말 stairno.com이 보낸 게 맞나"를 확인하는 장치 셋(SPF, DKIM, DMARC)을 DNS에 넣는 작업이다. 실제로 가입해서 Gmail로 받아 보니 받은편지함으로 왔고, 셋 다 통과 표시가 붙어 있었다.

운영에서 재 본 것

확인결과
가입, 로그인, 로그아웃통과
쿠키 설정자바스크립트 접근 불가, https 전용, stairno.com의 방끼리만 공유
보안 헤더통과
다른 출처에서 몰래 보낸 요청거절됨

그리고 문제 둘을 발견했다.

1) 무료 플랜 CPU 한도를 넘는다. 비밀번호 해시는 일부러 무거운 계산이다. 운영에서 재 보니 가입 한 번에 CPU 152ms, 로그인 약 95ms. Workers 무료 플랜의 요청당 한도 10ms를 한참 넘는다. 측정할 때는 요청이 성공했지만 한도를 넘은 채로 계속 운영할 수는 없다. 지금은 사용자가 나와 테스트 계정뿐이라 무료 플랜을 유지하고, 가입이나 로그인이 CPU 초과로 실패하기 시작하면 바로 유료 플랜(월 5달러)으로 바꾸기로 결정 기록에 적었다.

2) 한국에서 들어가면 미국을 거친다. 한국에서 보낸 요청이 Cloudflare의 로스앤젤레스 데이터센터(LAX)로 들어갔다. 왕복만 0.2초쯤이다. 이건 요금제와 통신사가 정해서 내가 바꿀 수 없다. 대신 할 수 있는 걸 했다. DB(Neon)는 AWS 싱가포르 리전에 있으니, API 서버를 DB 옆에서 돌게 설정했다.

요청(한국에서, 중앙값)전후
DB를 쓰지 않는 요청227ms384ms
상태 확인(DB 한 번)391ms371ms
공개 열쇠 목록(DB 여러 번)816ms418ms
로그인 확인(쿠키 없음)211ms399ms

공짜 개선이 아니다. DB를 여러 번 부르는 요청은 절반이 됐지만, DB를 안 부르는 요청은 싱가포르까지 한 번 더 돌아가서 느려졌다. 그래도 이쪽을 고른 이유는, 실제 API 요청은 거의 전부 DB를 한 번 이상 부르기 때문이다. 세션 확인도 매번 DB를 보고, 속도 제한 기록도 DB에 저장한다. DB를 안 부르는 건 로그인 안 한 사람의 "지금 누구야?"나 없는 주소 정도다.

화면 다듬기

운영 화면을 직접 보니 고칠 게 보였다.

  • 가입 완료 전인데 "완료" 안내가 같이 보였다. 숨긴 부분을 숨긴 대로 두게 고쳤다
  • 가입이 끝난 뒤에도 가입 화면 링크가 남아 있었다
  • 로그인과 가입 화면을 카드 하나에 담고, 허브의 밤하늘 띠와 점 무늬를 깔아 허브와 이어지게 했다

6. 허브와 같이 쓰는 계정 메뉴

오후에는 Stairno 1에서 말한 계정 메뉴를 붙였다. 순서가 중요해서 PR 하나씩 차례로 했다.

  1. 계정 메뉴 부품을 @stairno/ui에 만들고 버전을 올린다
  2. API가 허브(stairno.com)에서 오는 "지금 누구야?"와 "로그아웃" 요청을 받게 허용한다
  3. 허브에서 로그인하러 왔으면 로그인 뒤 허브로 돌려보낸다
  4. 계정 센터 헤더가 그 메뉴를 쓴다. 로고를 누르면 허브로 간다
  5. 허브가 그 메뉴를 쓴다(허브 레포에서)

단계마다 운영에서 직접 로그인해서 확인했다. 허브 첫 화면 오른쪽 위에서 로그인 상태가 보이게 됐다.

7. 테스트가 운영과 다른 조건에서 돌고 있었다

이 단계에서 찾은 것 중 제일 찜찜했던 것. Better Auth는 테스트 환경이면 출처 검사를 자동으로 끈다. 그래서 "다른 출처에서 온 로그인 요청은 거절한다"는 테스트가, 사실은 검사가 꺼진 상태에서 통과하고 있었다.

운영은 처음부터 막혀 있었다(실제로 다른 출처로 보내 보니 거절됐다). 하지만 테스트가 운영과 다른 조건이면 테스트를 믿을 수 없다. 설정으로 검사를 항상 켜 두게 고정해서, 테스트도 운영과 같은 조건에서 돌게 했다.

8. 인수 체크리스트

10월 18일 오후, 운영 주소에서 인수 체크리스트를 확인했다.

항목결과
1차 요구사항 전부자동 테스트(API 150개, 화면 134개, 종단 6개) + 운영에서 손으로 확인
인증 메일이 받은편지함으로Gmail 통과
쿠키, 보안 헤더통과
키보드만으로 가입·로그인·로그아웃, 360px 폭 화면통과
Lighthouse 접근성 100, 성능 90 이상접근성 100. 성능은 영어 91, 한국어 81~85로 미달. 글꼴 문제로 stairno-ui에 넘김
레포와 로그에 시크릿 없음통과. 세 레포 모두 비밀 값 자동 감지 경고 0
운영 OIDC로 실험 13단계 다시 돌리기미룸. 붙일 사이트가 없어서 5단계(Labs)에서
계정 삭제 뒤 연결된 사이트의 토큰 갱신 실패미룸. 같은 이유로 5단계에서. 자동 테스트는 통과

미룬 두 줄을 기억해 두자. "붙일 사이트가 없으니 나중에"라고 넘긴 이 확인이, 4a편에서 대가를 치른다.

3단계 끝. 다음은 결제다.

다음 글: Stairno 4. 결제 만들기