안녕하세요. 마리너 1기 이주성입니다.
이번 프로젝트에서는 GitHub Flow를 기반으로 Issue, 브랜치, 커밋, Pull Request, 코드 리뷰가 연결되는 협업 과정을 연습하고 있습니다. 동시에 웹 애플리케이션에서 자주 등장하는 라우팅, 인증·인가, 상태 관리 개념을 정리하며 프론트엔드와 백엔드, 데이터베이스가 어떻게 역할 분담하는지 이해하고 있습니다.
브라우저에는 Same-Origin Policy가 있습니다. 프로토콜·호스트·포트가 다르면 서로 다른 origin으로 보고, 기본적으로 스크립트가 다른 origin의 응답을 마음대로 읽지 못하게 합니다.
- CORS (Cross-Origin Resource Sharing): 서버가 특정 origin의 접근을 허용하겠다고 알려주는 메커니즘입니다. 프론트와 백엔드 포트가 다르면 CORS 설정이 필요해질 수 있습니다.
- 개발용 Proxy: 로컬에서 프론트 개발 서버가
/api같은 경로를 백엔드로 대신 전달하면, 브라우저는 같은 origin으로 요청한다고 인식합니다. Proxy가 CORS 정책 자체를 해제하는 것은 아니며, 브라우저가 같은 origin의 개발 서버로 요청하면 개발 서버가 이를 백엔드로 전달하는 방식입니다. 이 설정은 보통 로컬 개발 전용이며, 프로덕션 빌드에는 포함되지 않습니다. - 배포 환경의 Rewrites: 배포 플랫폼이 하나의 도메인 안에서 경로별로 정적 파일과 API를 나누어 응답하면, 브라우저 입장에서는 same-origin이 되어 CORS 이슈를 줄일 수 있습니다.
핵심은 “로컬에서 포트를 나누는 방식”과 “배포에서 경로로 나누는 방식”이 다르다는 점입니다.
서버리스는 요청이 들어올 때 함수(런타임)를 실행하는 방식입니다. 항상 켜져 있는 서버를 직접 운영하지 않는 대신, 다음 특성을 이해해야 합니다.
- Cold Start: 오랫동안 호출이 없다가 새로 런타임을 띄울 때 발생하는 초기 지연
- Warm Start: 이미 떠 있는 런타임을 재사용해 비교적 빠르게 응답하는 상태
- Stateless: 인스턴스 로컬에 중요한 상태를 오래 저장하기 어렵다는 전제. 세션 검증·데이터 저장은 외부 서비스(인증 서버, DB)에 맡기는 설계가 자연스럽습니다.
보안은 한 곳에서만 막으면 부족합니다. 계층을 나눠 역할을 분리하는 것이 기본입니다.
| 계층 | 개념 | 역할 | 한계 |
|---|---|---|---|
| UI Guard | 라우트 가드 | 로그인하지 않은 사용자의 화면 진입을 막음 | 브라우저 조작이나 API 직접 호출은 막지 못함 |
| API Auth | 서버 인증 | 요청의 토큰/세션을 검증하고 실패 시 401 반환 |
DB 권한 설계가 약하면 여전히 위험할 수 있음 |
| DB Security | GRANT + RLS | 테이블/행 단위로 접근 범위를 제한 | 정책이 잘못되면 과도한 차단 또는 과도한 개방이 발생 |
UI만으로 보안을 끝냈다고 생각하면 안 됩니다. 최종 권한 판단은 서버와 DB에서 해야 합니다.
- 인증: “이 사용자가 누구인가”를 확인하는 과정
- 인가: “이 사용자가 이 자원에 접근해도 되는가”를 확인하는 과정
토큰이 있다고 해서 바로 신뢰하면 안 됩니다. 특히 JWT는 페이로드가 Base64로 인코딩되어 있어 디코드만으로 내용을 읽는 것과 서명을 검증하는 것은 전혀 다릅니다.
- 위험한 접근: 클라이언트/서버에서 토큰을 단순 디코드해
user_id를 꺼내 사용 - 안전한 접근: 인증 서버에 토큰을 다시 물어보거나, 공개키(JWKS)로 서명을 검증하고 만료·폐기 여부까지 확인
서버리스처럼 상태를 오래 들고 있기 어려운 환경에서는, 인증 서버를 **단일 진실 공급원(SSOT)**으로 두고 세션 유효성을 위임하는 방식이 자주 사용됩니다.
- Bearer Token:
Authorization: Bearer <token>형태로 전달되는 자격 증명. “이 토큰을 가진 요청을 허용한다”는 의미입니다. - Publishable / Anon Key: 브라우저에 노출될 수 있는 저권한 키. 반드시 RLS 같은 행 단위 보안과 함께 써야 안전합니다.
- Service Role Key: RLS를 우회할 수 있는 고권한 키. 프론트엔드에 노출하면 안 되며, 필요할 때도 사용 범위를 최소화해야 합니다.
데이터베이스 보안은 보통 두 층으로 이해합니다.
- GRANT: 테이블(또는 스키마) 단위의 기본 권한. “이 역할이 이 테이블을 SELECT/INSERT 할 수 있는가”
- RLS (Row Level Security): 행 단위 필터. 예:
auth.uid() = user_id라면 “내 행만” 보이게 됩니다.
비유하면 GRANT는 건물 출입문, RLS는 각 호실 도어락입니다. 한쪽만 있으면 전체가 막히거나, 반대로 전체가 열릴 수 있습니다.
API 한 건을 안정적으로 다루려면 다음 개념이 중요합니다.
- Request ID: 요청마다 고유 ID를 붙여 로그·에러·타임아웃을 같은 흐름으로 추적
- 스키마 검증: 요청 body의 타입, 길이, 공백 여부를 서버에서 다시 검사 (클라이언트 검증만으로는 부족)
- 타임아웃: 외부 AI/HTTP 호출처럼 느린 작업에는 상한 시간을 두어 서버 자원을 보호
- 컨텍스트 윈도우: 대화형 API에서는 최근 N건만 가져와 모델 입력 크기를 제한하는 패턴이 흔함
- Optimistic UI: 서버 응답을 기다리기 전에 화면에 먼저 반영해 체감 속도를 높이는 기법. 실패 시 롤백 전략이 필요합니다.
useRef와 중복 요청 방지: React state 갱신은 렌더 사이클을 거치므로, 연속 클릭처럼 즉시 막아야 하는 플래그는 동기적으로 바뀌는 ref가 적합할 수 있습니다.sessionStorage: 탭 단위 UI 복원용 캐시에 가깝습니다. 서버가 가진 진짜 데이터(문맥, 권한)를 대체하지 않습니다.- 네임스페이스 키: 캐시 키에
user.id를 넣어 계정 전환 시 이전 사용자 데이터가 섞이지 않게 합니다. - 상태 수명 분리: 전역이 필요한 것(인증)과 지역적인 것(채팅 입력/말풍선)을 나누면 불필요한 리렌더와 복잡도를 줄일 수 있습니다.
헬스체크는 목적이 다릅니다.
- Liveness (
/health): 프로세스가 살아 있는지. 죽어 있으면 재시작 대상으로 봅니다. - Readiness (
/ready): DB, 외부 API 키처럼 의존성까지 준비됐는지. 살아 있어도 준비가 안 됐으면 트래픽을 받으면 안 됩니다.
이 구분을 두면 “앱은 떠 있지만 DB가 죽은 상태”를 더 정확히 격리할 수 있습니다.
초기에 복잡한 다대다 관계와 조인을 모두 넣기보다, 조회 패턴이 단순한 단건/로그성 테이블로 시작하는 경우가 있습니다. 문맥 조회가 “최근 성공 기록 N건”처럼 단순하다면, 테이블을 과도하게 정규화하지 않는 것도 의도적인 설계 선택이 될 수 있습니다. 대신 나중에 확장할 때 마이그레이션 비용을 감수해야 합니다.
- 웹 아키텍처·보안 관련 개념 학습 노트 작성
docs/CONTRIBUTING.md금지 사항·협업 흐름·관련 문서 섹션 추가 작업- Issue 기반 작업과 PR 연동 실습