'일단 돌아가니까 넘어가자'는 판단이 가장 위험한 순간은, AI가 그 코드를 작성했을 때다. Cursor나 Claude로 Next.js 프로젝트를 부트스트랩하면 30분 안에 그럴듯한 화면이 뜬다. 그런데 프로덕션에 올리고 나서야 이상한 버그들이 수면 위로 올라온다—번들이 이상하게 크고, 데이터가 업데이트되지 않고, 네비게이션 훅이 런타임에 터진다. 문제는 Next.js 자체가 아니다. App Router는 Pages Router와 근본적으로 다른 패러다임인데, AI가 생성한 코드는 종종 옛 습관을 새 프레임워크에 그대로 이식한다.
실제로 이 문제는 1인 개발 사례에서도 잘 드러난다. 최근 GeekNews에 공개된 AI 음악 생성 플랫폼 사례를 보면, 개발자 혼자 Next.js 16·React 19·Supabase·결제·소셜 로그인·PWA까지 전 스택을 구현했다. AI 도구 없이는 불가능한 속도였을 것이다. 하지만 개발자 스스로도 "만든 사람 입장에서 뭐가 어색한지 잘 안 보인다"고 토로했다. 빠르게 만드는 것과 올바르게 만드는 것 사이의 간극—그게 지금 AI 보조 개발이 풀어야 할 핵심 과제다.
함정 1. 'use client'를 페이지 최상단에 붙이기
App Router에서 모든 컴포넌트는 기본적으로 Server Component다. 클릭 핸들러 하나를 추가하려다 에러가 뜨면, 가장 쉬운 해결책은 파일 맨 위에 'use client'를 선언하는 것이다. AI도 똑같이 한다. 에러가 사라지니 '해결'처럼 보이지만, 이 선언이 페이지 루트에 붙는 순간 그 아래 임포트된 모든 컴포넌트가 클라이언트 번들로 빨려들어간다.
올바른 접근은 반대다. Server Component를 트리 최상단에 유지하고, 'use client'는 실제로 인터랙션이 필요한 가장 작은 리프 컴포넌트에만 붙인다. 버튼 하나 때문에 페이지 전체를 클라이언트로 내리는 대신, 그 버튼만 별도 파일로 분리하면 된다. AI에게 코드를 생성시킬 때 이 원칙을 프롬프트에 명시하지 않으면, 十중팔구 최상단에 'use client'가 붙어서 돌아온다.
함정 2. 네비게이션 훅을 잘못된 패키지에서 임포트하기
useRouter를 next/router에서 가져오는 건 Pages Router 시절의 반사 행동이다. App Router에서는 next/navigation을 써야 한다. 두 패키지의 API는 이름이 비슷해 보이지만 내부 동작이 완전히 다르고, 잘못 임포트하면 런타임 에러 또는 조용한 실패로 이어진다. AI가 생성한 코드에서 이 실수가 특히 자주 보이는 이유는, 학습 데이터에 Pages Router 코드가 훨씬 많기 때문이다.
useRouter, usePathname, useSearchParams 모두 next/navigation에서 가져와야 하며, 이 훅들은 Client Component 안에서만 동작한다. 이 두 가지 조건을 코드 리뷰 체크리스트에 명시적으로 넣어두지 않으면, AI가 만든 코드를 그대로 머지할 때 프로덕션에서 뒤늦게 발견하게 된다.
함정 3. Server → Client 경계를 넘는 직렬화 불가 데이터
Server Component에서 Client Component로 props를 넘길 때, 그 데이터는 JSON으로 직렬화 가능해야 한다. 함수, Date 객체, 클래스 인스턴스, DB 클라이언트 참조—이런 것들은 경계를 넘을 수 없다. 빌드 타임이나 렌더링 중 직렬화 에러로 터진다.
AI는 이 제약을 자주 무시한다. Server Component에서 콜백 함수를 props로 내려보내거나, raw Date 객체를 그대로 전달하는 코드를 생성하는 경우가 흔하다. 클라이언트에서 서버 액션을 트리거해야 한다면 props로 함수를 전달하는 대신 Server Actions를 사용하는 게 정석이다. props는 원시값, 순수 객체, 배열만으로 제한하는 것을 원칙으로 삼아야 한다.
함정 4. 초기 데이터 페칭을 여전히 클라이언트에서 하기
useEffect + useState로 데이터를 가져오는 패턴은 Pages Router 시절의 기본기였다. AI는 이 패턴을 매우 잘 생성한다—빠르고 에러 없이 작동하니까. 하지만 App Router에서 페이지의 주요 데이터를 이 방식으로 가져오면 로딩 스피너, 요청 워터폴, 불필요한 클라이언트 번들이 세트로 따라온다.
Server Component는 async 함수로 선언하고 그 안에서 직접 데이터를 페칭할 수 있다. DB 쿼리든 외부 API 호출이든 서버에서 처리하고 완성된 HTML을 브라우저에 내려보내면, 사용자 입장에서는 로딩 스피너 없이 콘텐츠가 즉시 뜬다. AI에게 "초기 데이터 페칭은 async Server Component로"라는 지시를 명확히 하지 않으면, 거의 반드시 클라이언트 사이드 페칭 코드가 생성된다.
함정 5. 캐싱 기본값을 모르고 쓰기
Next.js App Router의 캐싱은 강력하지만, 모르고 쓰면 데이터가 영구적으로 굳어버리는 버그가 된다. 기본 설정에서 fetch 요청은 빌드 타임 또는 첫 요청 시 캐시되며, 이후 동일한 응답을 계속 서빙한다. 사용자 알림, 실시간 가격, 재고 수량 같은 동적 데이터를 아무 설정 없이 페칭하면 페이지가 과거에 멈춰있게 된다.
해결책은 캐싱 의도를 명시적으로 선언하는 것이다. 캐시가 필요 없는 데이터라면 fetch 옵션에 { cache: 'no-store' }를 붙이거나, 페이지 파일 상단에 export const dynamic = 'force-dynamic'을 선언한다. Server Actions로 데이터를 변경한 뒤에는 반드시 revalidatePath() 또는 revalidateTag()를 호출해야 Next.js가 캐시를 갱신한다. AI는 이 부분을 거의 생략한다—기능은 작동하는데 데이터가 틀린 가장 전형적인 시나리오다.
함정 6. loading.tsx와 error.tsx를 빠뜨리기
App Router는 파일 기반 컨벤션으로 로딩 상태와 에러 핸들링을 제공한다. loading.tsx를 라우트 폴더에 두면 자동으로 Suspense 바운더리가 생성되고, error.tsx를 두면 클라이언트 에러 바운더리가 생성된다. 이 파일들 없이 비동기 렌더링을 하면, 서버가 처리를 마치는 동안 화면이 멈추거나 에러 발생 시 전체 레이아웃이 깨진다.
AI로 라우트를 생성하면 page.tsx는 만들어주지만 loading.tsx와 error.tsx는 종종 빠진다. 프롬프트에 "라우트 폴더에 loading과 error 파일도 함께 생성해줘"라고 명시하는 것이 습관이 되어야 한다. 그리고 생성된 error.tsx가 단순히 에러 메시지를 출력하는 수준인지, reset 함수를 활용해 재시도 UX를 제공하는지도 반드시 확인해야 한다.
AI 보조 개발에서 이 함정들이 특히 위험한 이유
Browser-based YAML 파서를 AI 보조로 개발한 사례(dev.to)에서 개발자는 솔직하게 고백했다. "AI를 주니어 개발자처럼 대했다. 명확한 지시를 주고 결과물을 전부 검토했다. '이걸 고쳐줘'가 아니라 왜 틀렸는지를 설명했다." 그 결과 AI가 만든 트리 뷰는 테이블 기반으로 엉망이었고 에러 핸들링은 날 것 그대로였다. 직접 다시 짜야 했다.
App Router 함정들은 바로 이 지점에서 더 위험하다. 잘못 짠 YAML 파서는 파싱이 틀리게 된다—즉시 눈에 보인다. 하지만 'use client'가 루트에 붙어도, 캐시가 고정되어도, 훅을 잘못된 패키지에서 임포트해도—일단은 돌아간다. 에러가 없다. 그래서 AI가 생성한 코드를 검토 없이 머지하기 가장 쉬운 영역이기도 하다.
빠른 프로토타이핑의 가치는 분명하다. 혼자서 창작·소셜·결제·어드민을 전부 구현하는 것, Next.js 스택 위에 AI 서비스를 빠르게 올리는 것—AI 도구 없이는 현실적으로 불가능한 속도다. 하지만 그 속도가 App Router의 구조적 원칙을 우회하는 방식으로 달성된다면, 기술 부채는 사용자가 늘수록 더 크게 돌아온다. AI에게 코드를 맡길수록, 개발자가 프레임워크의 원칙을 더 깊이 알아야 하는 역설—그게 지금 우리 앞에 놓인 현실이다.