0339

회고

[우테코] 12편 : 오픈미션 6일차

| 서론

안녕하세요 팡일입니다!

오늘은 우아한테크코스 8기 프리코스의 오픈미션 6일차 포스팅입니다. 오늘은 진행한 점들에 대해서 기록하고 공유하려고 합니다!

| 오늘 한 일

1) List페이지

  • 실제 본인 회고만 불러오도록 수정

2) Write페이지

  • userId를 실제 userId로 수정

  • userId까지 잘 저장되는지 검토

3) Login페이지

  • 로그인 페이지 뼈대 구현

  • 이메일 로그인 기능

  • 구글 로그인 기능

  • 비인증 사용자 접근 제한 예외처리

  • 로그인 시 발생 가능한 Firebase 오류 예외처리

4) Register 페이지

  • 회원가입 페이지 뼈대 구현

  • 이메일 회원가입 기능

  • 구글 로그인 회원가입 기능

5) Firebase

  • 사용자 컬렉션 추가

  • 랜딩페이지 기본 구성 만들어놓기

6) 라우팅 및 구조

  • 라우트 그룹 기능 활용하여 공개/비공개 페이지로 분리

  • 로그아웃 기능 추가

  • 세션 유지 강화

7) UI/UX 개편

  • 랜딩 페이지 도입

  • 전반적으로 카드 기반 레이아웃으로 통일

| 어려웠던 점

1) 비동기로 인한 세션 유지 실패

: Firebase의 인증 상태가 비동기적으로 복원된다는 점이 가장 큰 문제였습니다.

로그인 후 새로고침을 하면 세션이 유지되지 않고 /login으로 리다이렉트되는 현상이 계속 발생했습니다. 원인을 파악해보니, Firebase가 로그인 정보를 localStorage에 저장하긴 하지만 그 복원 과정(onAuthStateChanged)이 비동기적으로 처리되기 때문에 SSR 시점에는 아직 로그인 정보가 준비되지 않았던 것이었습니다.

SvelteKit은 페이지 새로고침 시 서버 렌더링이 먼저 실행되므로, auth.currentUser가 null 상태로 인식되어 “로그인이 안 되어 있다”고 판단해 /login으로 이동시키는 상황이 생긴 것이죠.

TypeScript
// src/routes/(private)/+layout.ts
import { redirect } from '@sveltejs/kit';
import { auth } from '$lib/firebase';
import { onAuthStateChanged } from 'firebase/auth';

export const load = async () => {
	if (typeof window === 'undefined') {
		return {};
	}

	const user = await new Promise((resolve) => {
		const unsubscribe = onAuthStateChanged(auth, (user) => {
			unsubscribe();
			resolve(user);
		});
	});

	if (!user) {
		throw redirect(307, '/login');
	}

	return {};
};

이 문제는 위의 코드 내의 Promise처럼, 결국 인증 복원 과정이 끝난 뒤에만 리다이렉트를 판단하도록 수정함으로써 해결했습니다.

2) Guard가 작동하지 않아 로그인 없이 접근 가능

: 처음 guard 로직을 추가했음에도 /list 페이지에 로그인 없이 직접 접근이 가능했습니다. 이유를 확인해보니, 모든 페이지가 동일한 +layout.svelte 아래에 존재해 “공개 페이지”와 “로그인 보호 페이지”를 구분할 수 없었던 것이 문제였습니다.

TypeScript
routes
├── (private)
│   ├── +layout.svelte
│   │   ├── +layout.ts
│   │   ├── detail
│   │   │   └── [id]
│   │   │       └── +page.svelte
│   │   ├── list
│   │   │   └── +page.svelte
│   │   └── write
│   │       └── +page.svelte
└── (public)
    ├── +layout.svelte
    ├── +page.svelte
    ├── login
    │   └── +page.svelte
    └── register
        └── +page.svelte

그래서 라우트 구조를 (public) (private) 으로 완전히 분리했습니다. 이제 /list, /write, /detail/...은 자동으로 (private) 그룹 아래로 포함되며, Firebase 인증이 없을 경우 guard가 /login으로 리다이렉트하게 되었습니다.

라우팅 구조를 재설계하면서 guard의 역할이 명확해졌고, 의도한 대로 접근 제어가 작동하기 시작했습니다.

3) Firebase 비밀번호 정책 위반 오류 처리

: 회원가입 기능을 구현할 때, Firebase에서 제공하는 createUserWithEmailAndPassword() 함수 호출 시 비밀번호가 너무 짧거나 단순할 경우 auth/weak-password 오류가 발생했습니다.

처음에는 이 에러를 Firebase의 응답 후 처리하려 했지만, UX 측면에서 즉각적인 피드백을 주는 것이 더 낫다고 판단했습니다.

그래서 호출 이전 단계에서 validatePassword() 함수를 통해 비밀번호 유효성을 먼저 검사하고, 문제가 있으면 바로 사용자에게 경고 메시지를 표시하도록 수정했습니다. 이 과정에서 입력 검증 로직의 위치와 타이밍이 얼마나 중요한지 체감할 수 있었습니다.

| 배운 점

1) 비동기 인증 복원의 원리 이해

: Firebase의 세션 복원은 단순히 저장된 토큰을 읽는 게 아니라 내부적으로 onAuthStateChanged를 통해 비동기로 인증 상태를 다시 초기화하는 과정임을 알게 되었습니다. 이에 따라 +layout.ts에서 세션 복원이 완료될 때까지 기다리는 로직을 추가했고, 이후에는 새로고침을 해도 로그인 상태가 안정적으로 유지되었습니다.

2) 라우팅 구조 분리의 중요성

: SvelteKit의 라우트 그룹 개념을 적극적으로 활용한 경험이었습니다. (public)과 (private)을 명확히 나눔으로써 각 페이지에서 인증 여부를 쉽게 구분할 수 있었고, 결과적으로 guard 로직이 훨씬 단순하고 예측 가능하게 작동했습니다. 이후로는 새로운 기능을 추가하더라도, 인증 구조를 건드리지 않아도 될 만큼 구조가 깔끔해졌습니다.

3) 클라이언트 단의 입력 검증 필요성

: Firebase가 내부적으로 정책 검증을 수행하더라도, 사용자 경험(UX) 측면에서는 클라이언트에서 즉시 피드백을 주는 것이 훨씬 자연스럽다는 걸 배웠습니다. 이전에는 서버 응답을 기다렸다가 오류를 처리했지만, 지금은 입력 단계에서 바로 경고를 표시하니 사용자는 문제를 즉시 인지할 수 있고, 불필요한 네트워크 요청도 줄어들었습니다.

| 내일 목표

1) 기타

  • 기능 명세서 수정하기

  • 우리 프로젝트에서 단위 테스트는 어떻게?

  • 모니터링 도구 알아보기

2) Write 페이지

  • 감정 버튼 기능 추가

  • 임시 저장 기능 추가

3) List 페이지

  • 날짜 정렬 기능 추가

  • 타이틀 검색 기능 추가

  • 개별 또는 전체 삭제 기능 추가

4) Update 페이지

  • 기본 UI 구성 조회 기능 추가

  • 수정 기능 추가

5) Detail 페이지

  • 삭제 기능 추가

  • 수정 페이지 이동 기능 추가

6) My 페이지

  • 기본 UI 구성

  • 정보 조회 기능 추가

  • 정보 수정 기능 추가

  • 회원 탈퇴 기능 추가

7) 리팩토링

  • 모듈화 가능한 부분 개선하기

| 한 줄 회고

: "점점 기능이 생기니 좋고, 꾸준히 기록하자"