Fullstack 94

heo4·2026년 8월 30일

Fullstack

목록 보기
49/70

풀스택

가계부 앱 "텅장" SEO, 게시판 기능, 그리고 Cloudflare 계정 지옥

하루 동안 세 가지를 했다. 구글/네이버 검색 노출 개선, 공지·문의 게시판 + 관리자 계정 추가,
그 와중에 터진 Cloudflare 다중 계정 자격증명 문제 해결.


1. "텅장"으로 검색해도 안 나온다 — SEO 손보기

증상

  • 구글 서치콘솔(GSC)에서 사이트맵이 한 달 넘게 "가져올 수 없음" 상태.
  • 구글에 "텅장"을 쳐도 사이트가 안 나옴.

진단

사이트맵 URL(/sitemap.xml)을 직접 요청해 보니 HTTP 200 + 정상 XML, Googlebot User-Agent로도 정상.
즉 파일 자체는 멀쩡했다. GSC의 "가져올 수 없음"은 처음 제출(약 한 달 전) 시점에 일시 오류가 났고,
그 뒤 구글의 재시도 주기가 느려서 방치된 상태였다. *.pages.dev 도메인에서 흔히 보고되는 증상.

검색이 안 되는 진짜 원인은 따로 있었다:
1. SPA라서 서버가 주는 HTML의 <div id="root">가 비어 있음 — 자바스크립트를 실행해야만 본문이 생김.
신규·저권위 사이트에서는 구글이 JS 렌더링을 뒤로 미루기 때문에 불리하다.
2. "텅장"이라는 붙여 쓴 문자열이 title·description·본문 어디에도 없었다. 전부 "텅~ 장"으로만 표기.
3. 구조화 데이터(JSON-LD)가 전혀 없어서 브랜드명을 인식할 근거가 부족.

조치 (index.html)

  • <title> / <meta name="description"> 에 "텅장" 표기를 자연스럽게 포함.
  • keywords, og:site_name, og:locale, twitter:card 등 메타 보강.
  • JSON-LD 추가: WebSite + SoftwareApplication, name: "텅장",
    alternateName: ["텅~ 장", "텅장 가계부"], applicationCategory: FinanceApplication.
  • <div id="root"> 안에 정적 히어로 마크업(로고 + h1 + 소개문 + 이용약관/개인정보 링크)을 넣음.
    React가 마운트되면서 createRoot().render()가 첫 렌더에 자식을 비우므로 사용자 화면엔 영향이 없고,
    크롤러는 JS 실행 없이도 핵심 텍스트를 읽는다.
  • <noscript> 블록에도 같은 취지의 소개 텍스트.
  • 네이버 서치어드바이저 소유확인용 <meta name="naver-site-verification" ...> 추가.

사용자(운영자)가 별도로 할 일

  • GSC에서 기존 사이트맵 삭제 후 재제출, URL 검사에서 색인 생성 요청.
  • 네이버 서치어드바이저 사이트 등록 → 소유확인 → 사이트맵 제출 + 웹 페이지 수집 요청.
  • 여유가 되면 커스텀 도메인 연결, 외부 링크 1~2개 확보(브랜드 검색 잡히는 속도가 크게 빨라짐).
  • 빙 웹마스터도구도 GSC에서 5분이면 가져오기 가능(빙 색인이 ChatGPT·코파일럿 검색에도 쓰임).

배운 점

  • "사이트맵을 못 읽는다"는 메시지는 실제로 파일이 멀쩡해도 뜬다. 라이브 URL을 직접 curl 해서 확인하는 게 먼저.
  • SPA + 신규 도메인 + .pages.dev 조합은 SEO에 3중으로 불리하다. 최소한 HTML에 브랜드명과 핵심 문구가
    정적으로 박혀 있어야 한다.

2. 공지 + 문의(Q&A) 게시판, 그리고 관리자 계정

요구사항

  • 공지사항과 Q&A를 하나의 폼으로.
  • 공지 작성 / 문의 답변은 관리자만. 관리자는 이메일 없이 별도 아이디로 로그인.
  • 문의 글은 글쓴이가 공개/비공개 선택.
  • 좌측 메뉴에 "게시판" 탭 추가.

설계

기존 앱에는 권한(role) 개념이 아예 없었다. 최소 변경으로:

DB (마이그레이션 1개)

  • users 에 is_admin INTEGER NOT NULL DEFAULT 0 컬럼 추가.
  • 관리자 계정 시드 1행. 이메일 없이 로그인하므로 email 컬럼(UNIQUE, 형식 제약 없음)을
    로그인 아이디 저장에 재사용. 비밀번호는 PBKDF2-SHA256 해시만 저장(평문은 저장소 밖).
  • board_posts 테이블 하나로 공지/문의를 같이 관리:
    type('notice'|'qna'), user_id, author_name, title, content,
    is_private, is_pinned, answer, answered_at, 타임스탬프.
    → Q&A 답변은 관리자 단일 답변(answer 컬럼). 별도 댓글 테이블 없이 단순하게.

백엔드 (Cloudflare Pages Functions)

  • functions/lib/admin.ts — isAdmin(env, userId) 헬퍼.
  • POST /api/auth/admin-login — 일반 로그인과 분리. 아이디+비번을 받고 is_admin=1 계정만 통과.
    아이디 없음/관리자 아님/비번 불일치를 모두 같은 에러 메시지로(계정 존재 여부 비노출).
  • me / login / register 응답의 user 객체에 is_admin 포함.
  • GET/POST /api/board, GET/PATCH/DELETE /api/board/[id]:
    • 공지: 관리자만 작성, 전원 공개, 상단 고정.
    • 문의: 로그인 사용자 누구나 작성, is_private면 작성자+관리자만 열람.
      목록·단건 조회에서 비공개 글 필터링. 답변/고정은 관리자만.

프론트 (React + TS)

  • AuthPage 에 "관리자 로그인" 모드 추가(하단 링크로 진입, 이메일 대신 아이디, 탭 숨김).
  • BoardView 신규 — 공지/문의 서브탭 + 단일 글쓰기 폼(유형은 서브탭을 따라감,
    문의만 비공개 체크박스) + 아코디언 목록 + 관리자 인라인 답변/고정, 작성자 수정/삭제.
  • App.tsx 좌측 메뉴에 'board' 탭(Megaphone 아이콘).

검증

tsc -b, lint, build 통과. 로컬 D1에 마이그레이션 적용 후 wrangler pages dev 로 실제 요청 테스트:

  • 관리자 로그인(성공/오답), 공지 작성 권한(일반 사용자 403 / 관리자 201),
  • 비공개 문의 격리(작성자·관리자만 열람, 타 사용자 403 + 목록에서 제외),
  • 관리자 답변·고정, 삭제 권한(타인 403 / 작성자 200), 잘못된 type 400.
    전부 기대대로 동작. 테스트 데이터는 정리.

3. 원격 DB에 마이그레이션이 안 올라간다 — Cloudflare 계정 지옥

코드는 git push → GitHub Actions가 Pages를 자동 배포. 문제는 D1 스키마는 자동 반영이 안 된다는 것.
wrangler d1 execute budget-db --remote --file=... 를 실행했더니:

✘ [ERROR] A request to the Cloudflare API (/accounts/558c8a68…/d1/database/…/import) failed.
  Authentication error [code: 10000]
👋 You are logged in with an User API Token, associated with the email asdf1378kk@gmail.com.

원인

  • budget-db 와 Pages 프로젝트는 계정 A (2@gmail.com, 558c8a68…799) 소속.
  • 그런데 ~/.bashrc 의 전역 CLOUDFLARE_API_TOKEN 이 계정 B (kk@gmail.com, 6209ea4d…985a)
    토큰으로 바뀌어 있었다. 언젠가 다른 프로젝트 작업하면서 갈아끼운 것.
  • 7월엔 계정이 맞아서 잘 됐던 게, 그 사이 토큰이 바뀌면서 깨진 것.

즉시 우회

대시보드 → Workers & Pages → D1 → budget-db → Console 탭에 마이그레이션 SQL을 직접 붙여넣어 실행.
토큰/계정 설정을 아예 안 건드려도 되는 가장 확실한 방법. IF NOT EXISTS / INSERT OR IGNORE 로
작성해 둬서 재실행해도 안전.

근본 해결

  1. 계정 A 대시보드에서 "Edit Cloudflare Workers" 템플릿으로 새 API 토큰 발급.
  2. ~/.bashrc 의 토큰 값을 계정 A 것으로 교체.
  3. wrangler whoami → Db848552@gmail.com 확인. d1 execute --remote 정상 동작 확인.

그런데 계정이 2개였다

조사해 보니 이 PC의 Cloudflare 프로젝트가 두 계정에 흩어져 있었다:

계정프로젝트
A: Db848552@gmail.com (558c8a68…799)budge, wedding-admin, wedd
B: asdf1378kk@gmail.com (6209ea4d…985a)ppnation, easyprompt, wedd-e9smwzxxan

전역 토큰은 하나뿐이라, 한 계정을 맞추면 다른 계정 프로젝트가 깨진다. 해결책:

  • 가끔 배포: 명령 앞에 자격증명을 붙인다.
    CLOUDFLARE_API_TOKEN=… CLOUDFLARE_ACCOUNT_ID=… npx wrangler deploy
  • 반복 작업: direnv 로 폴더별 .envrc 자동 적용.
    • sudo apt-get install -y direnv + ~/.bashrc 에 eval "$(direnv hook bash)"
    • 프로젝트마다 .envrc 에 토큰 + Account ID, direnv allow
    • .envrc 는 반드시 .gitignore 에 추가 (토큰이 커밋되면 안 됨)
  • 각 저장소(ppnation, easyprompt)에 DEPLOY.md 를 두어 "이 프로젝트는 어느 계정, 어떻게 실행"을 기록.

배운 점

  • 전역 CLOUDFLARE_API_TOKEN 은 머신 전체에 적용된다. 프로젝트별 설정이 아니다.
  • Cloudflare 계정이 2개 이상이면 처음부터 프로젝트별로 자격증명을 분리(direnv 또는 명령 프리픽스)하고,
    wrangler.toml 에 account_id 를 박아 계정을 명시하는 게 안전하다. (Account ID는 비밀이 아님)
  • GitHub Actions 배포와 로컬 CLI 배포는 자격증명이 완전히 별개다. Actions가 되니까 로컬도 될 거라고
    착각하면 안 된다.
  • D1 스키마 변경은 배포에 포함되지 않는다. 마이그레이션은 별도로 --remote 실행하거나 대시보드
    콘솔에서 돌려야 한다.

오늘 커밋 요약 (budge)

  • feat: 구글 검색 노출 개선 — "텅장" 브랜드 SEO 메타/구조화 데이터 추가
  • chore: 네이버 서치어드바이저 사이트 소유확인 메타태그 추가
  • feat: 공지/문의(Q&A) 게시판 + 관리자 계정 로그인
  • chore: 게시판/관리자 기능 배포 및 원격 D1 마이그레이션 적용 완료 기록
  • chore: .envrc gitignore 추가 + 다중 계정 자격증명 정리 기록

그 외 ppnation / easyprompt 저장소에 DEPLOY.md 추가.


TODO (남은 것)

  • GSC 사이트맵 삭제 → 재제출, 색인 생성 요청 (배포 반영 후)
  • 네이버 서치어드바이저 소유확인 + 사이트맵 제출
  • "텅장" 검색 노출은 색인 후 1~4주 소요 — 경과 관찰
  • 게시판: 배포된 앱에서 관리자 로그인 → 공지 작성 → 일반 계정으로 열람 최종 확인
  • (선택) 다른 계정 프로젝트가 잦으면 direnv 정식 세팅

0개의 댓글