백엔드프론트엔드

robots.txt 정적 전환 + middleware deep link 우회 + MY 탭 내 계정 카드 — POP-SPOT v2.15 (~v2.15.3)

김동현
··5분 읽기

v2.15.1 robots.txt 동적→정적 / v2.15.2 ?tab=MY 직접 진입 시 인트로로 튕기던 middleware 통과 / v2.15.3 MY 탭 상단에 회원 이름·이메일·프로필 사진 카드 추가. 작은 운영 보강 3 건 묶음.

v2.14 이후 큰 기능 도입 없이 운영 마찰을 줄이는 자잘한 세 가지 패치. robots.txt 응답 안정화, 딥링크가 인트로에서 튀던 버그, MY 탭 첫 화면에 내 계정 정보 노출.

이 글에서 다루는 것

  • robots.txt 를 Next.js 의 동적 라우트(app/robots.ts)에서 정적 파일(public/robots.txt)로 옮긴 이유
  • ?tab=MY 같은 쿼리 파라미터 딥링크가 middleware 에서 인트로로 튕기던 버그
  • 비공개 카드만 모인 MY 탭 첫 화면에 "내 계정" 카드 추가
  • 모르는 단어 한 줄로

    용어한 줄 설명
    robots.txt검색 엔진 크롤러에게 "어디는 들어와도 되고 어디는 들어오지 마" 알려주는 표준 파일
    동적 라우트요청이 올 때마다 서버가 응답을 계산해서 돌려주는 방식
    middleware (Next.js)모든 요청이 페이지에 도달하기 전에 통과하는 가운데 단계. 로그인 체크·리다이렉트에 쓰임
    deep link특정 화면으로 바로 진입하게 해주는 URL. 예) <code>?tab=MY</code>

    무엇이 바뀌었나

    버전변경
    v2.15.1robots.txt 동적 → 정적 파일
    v2.15.2middleware 가 <code>?tab=…</code> 쿼리를 가진 진입을 그대로 통과시킴
    v2.15.3MY 탭 상단에 "내 계정" 카드 — 이름·이메일·프로필 사진

    v2.15.1 — robots.txt 정적 전환

    코드와 해석

    파일: popspot-frontend/app/robots.ts (동적, 삭제 대상)

    typescript
    // v2.15.0 까지 — app/robots.ts (동적 라우트)
    import type { MetadataRoute } from 'next';
    
    export default function robots(): MetadataRoute.Robots {
      return {
        rules: [{ userAgent: '*', allow: '/', disallow: ['/api/'] }],
        sitemap: 'https://popspot.co.kr/sitemap.xml',
      };
    }

    해석 — app/robots.ts 는 Next.js 가 매 요청마다 객체를 직렬화해서 /robots.txt 응답으로 내보내는 라우트 핸들러. 결과는 정적인데 서버 단계 한 번이 끼어 있다. 크롤러가 자주 때리면 응답 지연 가능성이 있고, 일부 환경(Vercel ISR 캐시 miss)에서 한 번씩 404 가 났다.

    javascript
    # v2.15.1 — public/robots.txt (정적 파일)
    User-agent: *
    Allow: /
    Disallow: /api/
    Disallow: /music/passport
    
    Sitemap: https://popspot.co.kr/sitemap.xml

    해석 — public/ 안에 둔 파일은 빌드 결과물에 그대로 들어가서 정적 자산으로 서빙된다. Next.js / Vercel 둘 다 정적 파일은 CDN 캐시 친화적이라 응답 지연 0, 404 가능성 0.

    트레이드오프

  • 장점: 가장 안정적, CDN 캐시 적중
  • 단점: 환경별로 다른 sitemap URL 못 씀 (지금은 운영 도메인 하나뿐이라 무관)
  • 핵심 파일

  • popspot-frontend/app/robots.ts (삭제)
  • popspot-frontend/public/robots.txt (신규)

  • v2.15.2 — middleware 딥링크 우회

    사건 흐름

    사용자가 외부에서 https://popspot.co.kr/?tab=MY 링크를 누름 → 의도는 메인의 MY 탭으로 진입 → 그런데 인트로 페이지로 강제 리다이렉트 → 인트로 끝까지 보고 다시 메인 가서 MY 탭 누르는 두 번 일.

    원인

    middleware.ts 가 "메인 첫 방문이면 인트로 거치자" 룰만 단순하게 적용하느라 쿼리 파라미터가 있는 경우 를 구별 안 함.

    파일: popspot-frontend/middleware.ts (Next.js 전역 미들웨어)

    typescript
    // v2.15.1 까지 — middleware.ts. pathname 만 보고 query 은 무시
    export function middleware(req: NextRequest) {
      const visited = req.cookies.get('entered');
      // 위: 인트로 한 번이라도 거친 사용자는 이 쿼키가 채워짐
      if (!visited && req.nextUrl.pathname === '/') {
        // 위: 첫 방문 + 루트 경로 → 인트로 강제 이동
        //      문제: ?tab=MY 같은 쿼리가 있어도 pathname 은 여전히 '/' — 의도와 무관하게 인트로로 튔김
        return NextResponse.redirect(new URL('/intro', req.url));
      }
    }

    해석

  • req.cookies.get('entered') — 인트로 거치고 메인에 도달했음을 표시하는 쿠키 (없으면 "첫 방문")
  • pathname === '/' — 정확히 메인 경로일 때만
  • 문제: pathname 만 보고 searchParams 는 안 본다. ?tab=MY 가 있어도 "메인 첫 방문" 으로 분류되어 /intro 로 강제 이동
  • 수정

    파일: 같은 middleware.ts (v2.15.2 수정 후)

    typescript
    // v2.15.2 — 딥링크 화이트리스트 추가. 의도 명확한 진입은 인트로 건너뛰기
    const DEEP_LINK_KEYS = ['tab', 'open', 'q', 'region', 'period', 'category'];
    //                      ^^^^^  ^^^^^^  ^^^  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    //                      탭    모달   검색 BROWSE 필터 (지역·기간·카테고리)
    
    export function middleware(req: NextRequest) {
      const visited = req.cookies.get('entered');
      const hasDeepLink = DEEP_LINK_KEYS.some(
        (k) => req.nextUrl.searchParams.has(k)
        //                              ^^^^   이 키가 ?...= 안에 있는지만 체크 (값은 안 본다)
      );
      // 위: some() — 하나라도 맞으면 true. 어떤 딥링크라도 있으면 인트로 건너뛰기
    
      if (!visited && req.nextUrl.pathname === '/' && !hasDeepLink) {
        //                                            ^^^^^^^^^^^^   딥링크 없을 때만 인트로 강제 이동
        return NextResponse.redirect(new URL('/intro', req.url));
      }
    }
    // 결과: ?tab=MY 있으면 인트로 우회 → 마이페이지 1클릭 진입

    해석

  • DEEP_LINK_KEYS — "이 쿼리가 있으면 의도가 명확하니 인트로 건너뛰자" 의 화이트리스트. tab(메인 탭), open(모달), q(검색어), region/period/category(BROWSE)
  • searchParams.has(k) — 그 쿼리 키가 URL 에 있는지 검사 (값은 안 봄)
  • !hasDeepLink — 딥링크가 하나도 없을 때만 인트로로 보낸다
  • 결과: ?tab=MY 진입 → 인트로 우회 → 메인 + MY 탭이 한 번에 떴다.

    비하인드

    이런 종류의 버그는 보통 사용자가 카카오톡으로 내 사이트 링크를 공유했을 때 발견된다. 카카오톡이 OpenGraph 미리보기를 위해 URL 을 한 번 fetch 하는데 그게 인트로로 튕기면 미리보기가 인트로 화면. 메인 화면이 미리보기로 안 나오니 클릭률이 떨어진다. v2.20.3 의 봇 우회와는 다른 방향(사용자 의도 우회)이지만 결국 비슷한 결의 문제.

    핵심 파일

  • popspot-frontend/middleware.ts

  • v2.15.3 — MY 탭 "내 계정" 카드

    문제

    MY 탭에는 위시·코스·스탬프 같은 "내 활동" 카드는 있는데, 내 계정 정보 (이름·이메일·프로필 사진) 가 한 군데도 안 보였다. 사용자가 "이 사이트에 내 정보가 뭘로 저장되어 있지?" 를 확인하려면 "프로필 수정" 같은 별도 페이지로 들어가야 함.

    수정 — 작은 카드 한 장

    파일: popspot-frontend/src/app/page.tsx 의 MY 탭 컴포넌트 부분 (MyAccountCard 서브함수)

    typescript
    // v2.15.3 — MyPage.tsx 상단에 계정 정보 카드 추가
    function MyAccountCard({ user }: { user: User }) {
      return (
        <section className="rounded-2xl border border-zinc-200 p-4 mb-4
                            flex items-center gap-3 bg-white">
          <Avatar src={user.profileImageUrl} name={user.name} size={48} />
          <div className="flex-1 min-w-0">
            <p className="font-medium text-sm truncate">{user.name}</p>
            <p className="text-xs text-zinc-500 truncate">{user.email}</p>
          </div>
          <Link href="/profile/edit" className="text-xs text-lime-600">
            편집
          </Link>
        </section>
      );
    }

    해석

  • rounded-2xl border — popspot 의 표준 카드 스타일(둥근 16px + 회색 테두리)
  • flex items-center gap-3 — 아바타 / 텍스트 / 편집 링크를 가로 한 줄
  • flex-1 min-w-0 + truncate — 이름·이메일이 너무 길어도 카드 폭을 벗어나지 않게 "…" 처리
  • Link href="/profile/edit" — 카드 자체가 아니라 "편집" 링크만 클릭 가능 (전체를 누르게 했다면 의도치 않은 이동이 잦다)
  • 디자인 결정

    로그아웃 버튼을 카드 안에 넣고 싶은 유혹이 있었지만 분리. 로그아웃은 "실수 클릭 위험" 이 있는 동작이라 다른 영역(헤더 또는 설정)에 두는 게 표준. MY 카드는 "보여주는" 역할만.

    핵심 파일

  • popspot-frontend/src/app/page.tsx (MY 탭 컴포넌트 부분)

  • 관련 글

  • 이전 — v2.14, 음악 검색 cover/live/remix 우회
  • 다음 — v2.16, 프로필 편집 + Header 아바타
  • 공유

    댓글