Next.jsnext-intlSEOmiddleware

Next.js + next-intl, /ko/ 리디렉션 오류와 proxy.ts 충돌 삽질기

May 21, 20261 min read

Google Search Console을 보다가 이런 항목이 눈에 들어왔다.

리디렉션 오류 — 영향받은 페이지 1개
https://backtodev.com/ko/

사이트는 잘 돌아가고 있었다. /ko 로 접근하면 홈이 뜨고, 포스트도 잘 보인다. 그런데 /ko/ (trailing slash 붙은 버전)에서 리디렉션 오류가 난다고 한다.

원인을 찾다가 middleware.ts를 추가했는데, 오히려 빌드 에러가 터졌다. 알고 보니 이미 proxy.ts가 있었다.


1차 시도 — middleware.ts 추가 (삽질)

Next.js + next-intl 구조를 살펴보니 middleware.ts가 없었다. next-intl은 로케일 라우팅을 위해 미들웨어가 필요하다. 바로 추가했다.

// middleware.ts (새로 생성)
import createMiddleware from "next-intl/middleware";
import { routing } from "./i18n/routing";

export default createMiddleware(routing);

export const config = {
  matcher: [
    "/",
    "/(ko|en)/:path*",
    "/((?!_next|_vercel|api|.*\\..*).*)",
  ],
};

커밋하고 푸시했더니 Vercel 빌드가 터졌다.

Error: Both middleware file "./middleware.ts" and proxy file "./proxy.ts"
are detected. Please use "./proxy.ts" only.

proxy.ts가 이미 존재한다는 것을 몰랐다.


원인 파악 — proxy.ts가 이미 있었다

프로젝트 루트에 proxy.ts 파일이 있었다. Next.js의 특정 버전에서는 middleware.ts 대신 proxy.ts를 미들웨어 파일로 사용한다. 두 파일이 동시에 존재하면 빌드가 실패한다.

proxy.ts 내용을 확인해보니, 이미 next-intl 미들웨어가 포함되어 있었다.

// proxy.ts
import { NextRequest, NextResponse } from "next/server";
import createIntlMiddleware from "next-intl/middleware";
import { routing } from "./i18n/routing";
import { verifyToken } from "./lib/auth";

const intlMiddleware = createIntlMiddleware(routing);

export async function proxy(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // 인증 API는 항상 허용
  if (pathname.startsWith("/api/admin/auth")) return NextResponse.next();

  // /admin 페이지 보호
  if (pathname.startsWith("/admin")) {
    if (pathname === "/admin/login") return NextResponse.next();
    const token = request.cookies.get("admin_token")?.value;
    const valid = token ? await verifyToken(token) : false;
    if (!valid) {
      return NextResponse.redirect(new URL("/admin/login", request.url));
    }
    return NextResponse.next();
  }

  // 나머지는 next-intl 처리
  return intlMiddleware(request);
}

export const config = {
  matcher: ["/((?!_next|.*\\..*).*)", "/api/admin/:path*"],
};

마지막 줄 return intlMiddleware(request) — 어드민 경로가 아닌 모든 요청은 next-intl 미들웨어가 처리하고 있었다. /ko/ trailing slash 처리도 이미 여기서 담당 중이었다.


proxy.ts 구조 이해

이 프로젝트의 미들웨어는 두 가지 역할을 동시에 한다.

요청 들어옴
    ↓
proxy.ts 가로챔
    ├── /api/admin/auth → 통과 (로그인/로그아웃 API)
    ├── /admin/login → 통과
    ├── /admin/* → 토큰 검증 → 실패 시 /admin/login 리다이렉트
    ├── /api/admin/* → 토큰 검증 → 실패 시 401
    └── 나머지 전부 → createIntlMiddleware 처리
                          ├── / → /ko 리다이렉트
                          ├── /ko/ → /ko 정리
                          └── /알수없는경로 → /ko/알수없는경로

middleware.ts(일반적인 next-intl 설정)와 다른 점은 어드민 보호 로직이 앞단에 결합되어 있다는 것이다. 별도 파일로 분리하면 두 미들웨어가 충돌하기 때문에 하나로 합쳐놓은 구조다.


해결 — middleware.ts 삭제

내가 만든 middleware.ts를 그냥 삭제했다. proxy.ts가 이미 모든 걸 처리하고 있었으니까.

rm middleware.ts
git add middleware.ts
git commit -m "fix: 중복 middleware.ts 제거 — proxy.ts에 이미 intl middleware 포함"
git push origin main

빌드 성공. /ko/ 리디렉션도 proxy.tsintlMiddleware가 원래부터 처리하고 있었다.


배운 것

기존 파일부터 확인하자. 에러를 보고 새 파일을 추가하기 전에, 이미 비슷한 역할을 하는 파일이 있는지 먼저 살펴봤어야 했다.

Next.js 버전에 따라 middleware 파일명이 다를 수 있다. 일반적으로는 middleware.ts지만, 이 프로젝트처럼 proxy.ts를 쓰는 경우도 있다. 두 파일이 동시에 존재하면 빌드 자체가 실패한다.

next-intl은 다른 미들웨어 로직과 합칠 수 있다. createIntlMiddleware가 반환하는 함수를 직접 호출하는 방식으로, 어드민 인증 같은 커스텀 로직과 하나의 파일에 깔끔하게 통합할 수 있다.


정리

Google Search Console: /ko/ 리디렉션 오류 발생
    ↓
middleware.ts 없다고 판단 → 새로 생성
    ↓
빌드 에러: proxy.ts와 middleware.ts 동시 존재 불가
    ↓
proxy.ts 확인 → 이미 createIntlMiddleware 포함되어 있음
    ↓
middleware.ts 삭제 → 빌드 성공
    ↓
/ko/ 리디렉션은 원래부터 proxy.ts가 처리 중이었음

Search Console 오류는 Google이 재크롤링하면 자연스럽게 해소된다.

PM

backtodev

40대 PM, 다시 개발자로 돌아갑니다. 실패하고 배우며 성장하는 기록.

Next.js + next-intl, /ko/ 리디렉션 오류와 proxy.ts 충돌 삽질기 | backtodev