Tesla Guide 보안·SEO·이미지 로딩 개선 기록

React
날짜 없음

Tesla Guide 보안·SEO·이미지 로딩 개선 기록

오늘은 Tesla Guide의 API 키 보안, 검색 색인, 이미지 생성 오류 처리, 갤러리와 게시글 로딩 성능을 개선했다.

1. Gemini API 키 암호화

기존에는 로그인 사용자의 Gemini API 키가 Firestore users 문서에 평문으로 저장되고 있었다.

이를 다음 구조로 변경했다.

  • AES-256-GCM 암호화
  • 암복호화는 Next.js 서버에서만 수행
  • 브라우저에는 저장된 키를 다시 반환하지 않음
  • Firestore에는 ciphertext, iv, authTag, version만 저장
  • 이미지 생성 시 서버가 잠시 복호화해 Gemini API 호출
  • 클라이언트에서 암호화 필드를 직접 수정하지 못하도록 Firestore Rules 강화

기존 평문 키 52개도 마이그레이션을 통해 암호화했으며 평문 필드는 삭제했다.

암호화 키는 NEXT_PUBLIC_이 붙지 않은 서버 환경변수로 관리한다.

GEMINI_KEY_ENCRYPTION_KEY=서버_전용_암호화_키

이 키를 변경하거나 분실하면 기존 암호문을 복호화할 수 없으므로 별도 보관이 필요하다.

2. AI 이미지 생성 실패 처리

기존에는 Gemini 이미지 생성이 실패해도 예제 이미지를 반환하면서 HTTP 200과 success: true를 응답했다.

이 때문에 사용자는 예제 이미지를 실제 생성 결과로 오인할 수 있었고, 완료 문구와 다운로드 버튼까지 활성화됐다.

현재는 실패 원인에 따라 상태 코드를 구분한다.

  • API 키 없음 또는 잘못된 키: 400
  • 로그인 인증 만료: 401
  • API 사용량 또는 크레딧 소진: 429
  • Gemini 서비스 및 이미지 생성 장애: 502

생성에 실패하면:

  • 예제 이미지를 표시하지 않는다.
  • 이전 생성 결과를 제거한다.
  • 완료 문구와 갤러리 안내를 표시하지 않는다.
  • 다운로드 버튼을 표시하지 않는다.
  • 화면에 실패 원인을 명확하게 안내한다.

3. Google 색인 문제 개선

일부 페이지가 자신의 URL이 아닌 홈페이지를 canonical로 지정하고 있었다.

영향을 받던 주요 페이지:

  • /guide
  • /learn
  • /about
  • /faq
  • /gallery
  • /contact
  • /privacy
  • /terms

각 페이지가 자기 자신을 canonical로 지정하도록 수정했다.

실제로 동작하지 않던 쿼리 기반 hreflang 선언도 제거했다. 언어는 쿠키와 요청 헤더로 결정되는데 ?lang=en 같은 URL을 hreflang으로 선언하고 있어 검색엔진에 잘못된 신호를 보내고 있었다.

추가로 다음 항목도 개선했다.

  • 중복 robots.txt 제거
  • 사이트맵의 오래된 캐시 제거
  • 콘텐츠가 바뀌지 않아도 매번 변경되던 lastModified 제거
  • 존재하지 않는 게시글을 HTTP 200으로 반환하던 soft-404 수정
  • 없는 게시글은 실제 HTTP 404 반환
  • slug 중복 URL은 대표 ID URL로 영구 리다이렉트
  • 헤더에 중복으로 존재하던 <h1> 제거
  • 게시글 Article 구조화 데이터 정리

4. 게시글 서버 렌더링

기존 게시글 목록은 브라우저에서 Firestore 조회가 끝난 뒤에야 나타났다.

검색엔진이 처음 받은 HTML에는 게시글 링크가 없었으며, JavaScript 실행에 실패하면 게시글을 발견할 수 없는 구조였다.

현재는 서버에서 공개 게시글을 조회해 초기 HTML에 포함한다.

  • 게시글 제목과 링크가 초기 HTML에 포함
  • 페이지 번호를 /posts?page=2 형태의 실제 URL로 제공
  • 검색엔진이 2페이지 이후 게시글도 탐색 가능
  • 상세 페이지 본문과 메타데이터 서버 렌더링
  • 비공개 게시글은 목록과 사이트맵에서 제외

5. 갤러리 서버 렌더링

갤러리도 기존에는 다음 순서로 동작했다.

  1. HTML 다운로드
  2. React 실행
  3. 갤러리 API 요청
  4. 목록 응답
  5. 이미지 다운로드 시작

현재는 서버가 페이지 데이터를 조회해 HTML에 바로 포함한다.

  • 첫 HTML에 갤러리 9개 포함
  • /gallery?page=2 형태의 서버 페이지 제공
  • 첫 3개 썸네일 우선 로딩
  • API 응답을 기다린 후 이미지 요청을 시작하던 지연 제거
  • 페이지별 canonical 적용

6. 이미지 최적화

갤러리와 게시글은 최대 1~2MB에 가까운 원본 PNG를 목록에서 그대로 내려받고 있었다.

Firebase Storage 응답도 private, max-age=0이어서 재방문 시 이미지를 다시 요청해야 했다.

개선 내용:

  • 갤러리 업로드 시 원본과 WebP 썸네일 분리
  • 원본은 다운로드용으로 유지
  • 목록에서는 최대 960×540 WebP 썸네일 사용
  • public, max-age=31536000, immutable 캐시 적용
  • 갤러리와 게시글 이미지를 Next Image로 전환
  • 화면 크기에 맞는 이미지 크기 제공
  • 첫 화면 이미지만 우선 로딩
  • 삭제 시 원본과 썸네일을 함께 제거

기존 갤러리 이미지 18개도 모두 변환했다.

  • 평균 썸네일 크기: 약 56KB
  • 최대 썸네일 크기: 약 78KB
  • 기존 게시글 Storage 이미지 9개에도 장기 캐시 적용

7. 갤러리 데이터 조회 개선

기존 갤러리 API는 요청할 때마다 전체 Firestore 컬렉션을 읽고 서버 메모리에서 정렬과 페이지 분할을 수행했다.

현재는 Firestore에서 직접 처리한다.

  • createdAt 기준 내림차순 정렬
  • 현재 페이지에 필요한 문서만 조회
  • count aggregation으로 전체 개수 조회
  • 마이페이지도 사용자 조건과 페이지 조건을 DB 쿼리에 적용

데이터가 증가할수록 서버 메모리와 Firestore 읽기 비용 차이가 커질 것으로 예상된다.

검증

다음 검증을 완료했다.

  • ESLint 통과
  • Next.js 프로덕션 빌드 통과
  • 갤러리 1·2페이지 SSR 확인
  • 게시글 목록 초기 HTML 링크 확인
  • 게시글 상세 구조화 데이터 확인
  • 존재하지 않는 게시글 HTTP 404 확인
  • canonical 및 sitemap 응답 확인
  • 갤러리 썸네일 18개 생성 확인
  • Gemini 평문 API 키 52개 암호화 확인

이번 업데이트는 단순 UI 수정이 아니라 사용자 비밀정보 보호, 검색엔진 접근성, 실패 응답의 정확성, 첫 화면 로딩 성능을 함께 개선한 작업이었다.

개선전 스피드 인사이트 점수

개선전 이미지1
개선전 이미지2

개선후 스피드 인사이트 점수

개선후 이미지1
개선후 이미지2

댓글

Top