디폴트 답변의 함정
누군가 "React 앱 어떻게 시작해?"라고 물으면, 대부분의 대답은 Next.js다. 틀린 말은 아니다. 하지만 dev.to에 최근 올라온 두 편의 글을 나란히 읽으면 불편한 사실이 드러난다. 하나는 Next.js의 Server Actions 보안 취약점을 집요하게 파고들고, 다른 하나는 팀 전체가 Next.js를 걷어내고 Vite SPA로 이전한 경험을 담고 있다. 두 글이 동시에 가리키는 건 동일한 질문이다. "이 프로젝트에 Next.js가 정말 필요한가?" 그리고 필요하다면, "지금 열려 있는 구멍을 알고는 있는가?"
먼저 '무엇을 만드는가'를 정직하게 답하라
dev.to의 "You don't need Next.js" 글은 단순한 Next.js 비판이 아니다. 저자가 운영하던 제품은 대시보드, 필터, 차트로 가득 찬 인증 기반 B2B 앱이었다. 거의 모든 화면이 로그인 뒤에 있고, 인터랙션에 따라 실시간으로 데이터가 바뀌는 구조. 이런 앱에서 SSR이 실제로 가져다준 건 초기 렌더 속도 향상이 아니라 디버깅 복잡도 증가, 인증 로직의 서버-클라이언트 이중화, 그리고 테스트 불가능한 Server Component 구조였다.
결국 팀은 결정 기준을 하나로 정리했다. 콘텐츠 우선(Content-first)인가, 애플리케이션 우선(Application-first)인가. 마케팅 사이트, 블로그, 이커머스 스토어처럼 퍼블릭 페이지가 많고 SEO가 핵심이라면 Next.js는 진짜 강점을 발휘한다. 반면 인증 뒤에 숨어 있는 어드민 패널, SaaS 콘솔, 내부 대시보드라면 SSR이 해결하는 문제보다 만들어내는 문제가 더 많다. 이 구분 없이 프레임워크를 고르는 건, 짐 무게를 모르고 차를 고르는 것과 같다.
그래도 Next.js를 쓴다면, Server Actions는 API 엔드포인트다
Next.js를 선택했다면 이제 두 번째 질문이 온다. 그리고 여기서 많은 팀이 조용히 구멍을 만든다.
dev.to의 Server Actions 보안 글 저자는 코드 리뷰에서 반복적으로 같은 패턴을 발견했다고 말한다. 페이지 컴포넌트에는 세션 검증이 붙어 있다. 그런데 그 페이지의 폼이 호출하는 Server Action에는 인증 체크가 없다. 논리적으로는 "이미 페이지에서 검증했으니까"라고 느끼지만, 'use server'는 인증을 추가하지 않는다. HTTP 엔드포인트를 노출할 뿐이다.
유효한 세션 쿠키와 cURL 한 줄이면 UI 없이도 해당 Action을 직접 호출할 수 있다. 버튼이 렌더링되는 페이지가 보호돼 있다는 사실은 아무런 방어막이 되지 않는다. Next.js 공식 문서도 명확히 적고 있다. "Server Actions를 공개 API 엔드포인트와 동일한 보안 기준으로 다루라."
가장 흔한 실수: 페이지 인증과 Action 인증을 같다고 착각하기
취약한 패턴은 단순하다. 페이지 컴포넌트에서 verifySession()을 호출해 사용자를 확인하고, 그 페이지에 연결된 Server Action에서는 세션 검증을 생략하는 것이다. 페이지 렌더링 시점의 인증과 Action 실행 시점의 인증은 완전히 독립적인 이벤트다.
수정은 간단하다. Action 내부에서도 verifySession()을 첫 줄에 호출하고, 리소스 소유권까지 검증한다. 게시물 삭제라면 해당 포스트의 authorId가 세션의 userId와 일치하는지를 Action 안에서 직접 확인해야 한다. 그리고 "찾을 수 없음"과 "권한 없음"은 같은 에러 메시지로 처리한다. 서로 다른 에러를 반환하면 공격자가 어떤 리소스 ID가 존재하는지 추론할 수 있다.
Data Access Layer: 인증 로직을 한 곳에 모으는 이유
매 Action마다 소유권 검증 코드를 복붙하다 보면 필연적으로 한 곳을 수정하고 나머지를 빠뜨리는 순간이 온다. 이 문제를 구조적으로 해결하는 패턴이 Data Access Layer(DAL)다.
핵심은 두 가지다. 첫째, 모든 인증·소유권 검증·데이터 접근 로직을 server-only 마킹이 붙은 별도 모듈에 집중시킨다. server-only는 단순한 관례가 아니다. 이 모듈이 클라이언트 컴포넌트에서 임포트되면 빌드 자체가 실패한다. 인증 로직이 브라우저에 도달하는 경로를 물리적으로 차단한다. 둘째, Server Action은 최대한 얇게 유지한다. Action이 하는 일은 입력값 파싱, DAL 함수 호출, 캐시 무효화(revalidatePath) 세 가지가 전부다. 비즈니스 로직과 인증은 DAL 안에 있다.
React의 cache() 래퍼를 verifySession()에 감싸두면 같은 렌더 패스에서 여러 번 호출돼도 쿠키 읽기와 JWT 검증이 한 번만 실행된다. 성능과 일관성을 동시에 잡는 작은 디테일이다.
Layout 인증의 함정도 실전에서 자주 걸린다
공유 Layout에 인증 체크를 넣으면 효율적으로 보인다. 한 번만 검증하면 그 아래 모든 라우트가 보호되는 것처럼. 그런데 Next.js의 Partial Rendering 특성상 Layout은 같은 세그먼트 내 페이지 이동 시 재렌더링되지 않는다. /dashboard에서 세션을 검증한 뒤 /dashboard/billing으로 이동해도 Layout은 다시 실행되지 않는다. 그 사이에 세션이 만료되거나 폐기돼도 billing 페이지는 그냥 열린다.
해결책은 Layout의 역할을 명확히 구분하는 것이다. Layout은 사용자 이름이나 아바타 같은 표시용 데이터만 가져온다. 실제 인가(Authorization) 체크는 각 페이지 컴포넌트 또는 DAL 함수 안에서 이뤄져야 한다.
시사점: 선택과 설계는 한 묶음이다
두 글을 함께 읽으면 하나의 교훈으로 수렴된다. 프레임워크 선택은 보안 설계의 전제 조건이다. Next.js를 쓰지 않아도 될 앱에 굳이 Server Actions를 도입하면, 없어도 될 공격 표면을 만드는 셈이다. 반대로 Next.js가 진짜 필요한 앱에서 Server Actions를 API 엔드포인트로 다루지 않으면, 편리함 뒤에 조용히 구멍이 생긴다.
프로덕트 관점에서 정리하면 이렇다. 먼저 "이 앱의 가치는 퍼블릭 콘텐츠에 있는가, 인터랙티브한 사용자 경험에 있는가"를 질문한다. 전자라면 Next.js의 SSR/SSG가 실질적인 사용자 가치를 만든다. 후자라면 Vite SPA가 복잡도와 유지보수 비용을 낮추면서 더 빠른 실험을 가능하게 한다. 그리고 Next.js를 선택한 순간부터 Server Actions는 버튼 뒤의 내부 로직이 아니라 독립적인 HTTP 엔드포인트로 다룬다. DAL 패턴으로 인증 로직을 중앙화하고, Layout에 인가를 위임하지 않는다.
전망: 복잡도 비용을 계산하는 팀이 이긴다
Next.js는 계속 진화하고 있고, React Server Components는 서버-클라이언트 경계를 더 세밀하게 제어할 수 있는 방향으로 발전 중이다. 하지만 기술이 정교해질수록 그 복잡도를 관리하는 팀의 역량 격차도 벌어진다. Vite SPA로 이전한 팀이 증명한 건 Next.js가 나쁜 도구라는 게 아니다. 도구의 복잡도 비용을 정직하게 계산하는 팀이 불필요한 짐을 내려놓을 수 있다는 것이다.
가장 실용적인 접근은 이 두 질문을 프로젝트 킥오프 때 명시적으로 답하는 것이다. "이 앱에서 SSR이 해결하는 사용자 문제는 무엇인가?" 그리고 "Server Actions를 쓴다면, 각 Action을 독립 API 엔드포인트로 설계할 준비가 돼 있는가?" 이 두 질문에 명확히 답할 수 있다면, 프레임워크 선택과 보안 설계 모두 훨씬 단단해진다.