어려운 Next15
Next15가 출시한 뒤 프로젝트에 도입하며 이게 과연 프레임워크로써 가치가 있는지 혹은 아
니면 사용 할 수 밖에 없는 계륵같은 존재인가? 라는 생각을 많이 하였습니다.
보안에 좋지만 사용하기는 매우 어려웠는데 프로젝트에 도입하며 발생 했던 문제점 혹은 사
용하기 어려웠던 점을 정리하려고 합니다.
1. 서버
next서버 에서 백엔드로 api를 호출 하는 방법에는 3 가지가 있습니다. (제가 알고 있는
방식에는)
첫 번째로 서버 액션
두 번째로 서버 컴포넌트에서 fetch api로 백엔드 api호출
app router로 api를 구현하여 해당 api로 요청을 보내 백엔드 api 호출
API 호출 방식
서버 컴포넌트는 서버에서 처음 작동하지만 서버의 기능을 완벽하게 사용하지 못합니
다.
즉 다시 말해 서버 컴포넌트는 백엔드의 api를 호출하여 데이터를 받아 온 다음 해당 데
이터를 이용하여 정적 페이지를 생성하여 내려 주는 역할이며 서버의 보안적인 기능은
사용하지 못합니다.
예를 들어 서버 액션을 사용하여 cookie 값을 수정 할 수는 있지만 서버 컴포넌트는 쿠
키를 읽을 수는 있지만 쿠키의 값을 수정하지는 못 합니다.
서버 액션은 클라이언트의 로직 실행 중에 Next.js 서버로 요청을 보내 Next.js 서버에
서 특정 로직을 실행 하기 위한 기능 입니다. 백엔드 에서 제공하는 api와 비슷한 맥락이
라 생각 하면 됩니다.
실제로 Next.js 에서는 페이지를 요청할 때는 Next 서버에 GET 요청을 보내고, 서버
액션을 호출 할 때는 POST요청을 보내 서버에서 로직을 수행 할 수 있도록 동작 시킵니
다.
app router로 api를 구현하는 방법은 스프링이나 nest 서버 처럼 http method를 정
의하고 외부에서 해당 api를 호출 할 수 있도록 만드는 로직 입니다. (물론 next 클라이
언트 에서도 호출 됩니다)
첫 번째 문제점
보안적인 문제로 인해 next 서버 컴포넌트 에서는 쿠키를 삭제 하거나 수정하지 못합니
다.
그러면 실제 백엔드 서버에서 중복 로그인을 막거나 토큰이 만료가 됐을 때는 401 에러
등 권한 관련 에러가 발생 할 것인데 이러한 에러가 발생 했을 때 토큰을 지우고 로그인
페이지로 보내야 하는데 보안적인 문제로 next 프레임워크에서 막고 있다 보니 막을 수
가 없는 문제점이 있었습니다.
해결 방법
서버 컴포넌트에서 호출한 api의 response에 대한 error status값을 받아 throw를 던
지면 next.js는 next.js의 자체 error boundary 컴포넌트(error boundary는 클라이
언트 사이드로 동작합니다.)로 보내게 됩니다. 해당 컴포넌트에서 에러에 대한 status
값으로 서버 액션을 실행 시켜 토큰을 지우고 로그인 페이지로 redirect 시켜서 해결 하
였습니다.
두 번째 문제점
위의 첫 번째 방식으로 해결을 한 뒤 빌드를 하고 실행 시켰을 때 다음과 같은 에러가 나
왔습니다.
Error: An error occurred in the Server Components render.
The specific message is omitted in production builds to avoid leaking sensitive
A digest property is included on this error instance which may provide additio
details about the nature of the error.
서버에서 발생한 에러에 대한 상세 내용은 보안적인 이슈로 클라이언트 사이드로 전달
할 수 없게 해놓은 문제인데 해당 내용이 로컬에서 개발 환경으로 실행 할 때는 발생하지
않지만 빌드 된 파일을 실행 시켰을 때는 보안적인 이슈로 해당 에러가 발생하면서 에러
status code 및 에러 메시지를 볼 수 없었다.
해결 방법과 시행착오
첫 번째로 해결 하려고 시도 했던 방법은 서버 컴포넌트 에서도 서버 액션을 호출 하면
되지 않을까? 라는 생각으로 서버 컴포넌트에서 해봤지만 서버 컴포넌트에서 실행 시키
는 서버 액션은 서버 컴포넌트 에서 함수를 하나 불러와 실행 하는 것과 같기 때문에 보
안적인 부분에 대해 수정하거나 삭제 할 수 없는 이슈가 그대로 발생 했습니다.
두 번째로 기존의 error 클래스가 아니라 새로 에러 클래스를 정의 하고 해당 에러를
throw 하면 되지 않을까?
해당 부분을 시도 해봤을 때는 두 가지 방식으로 시도를 했습니다.
a첫 번째는 error 클래스를 새로운 클래스로 상속 받아서 에러 클래스를 새로 정의
하는 방법
→ 해당 방식을 했을 때는 역시 똑같이 위에서 발생한 서버에서 클라이언트로 상세
에러 내용을 전달 할 수 없다는 메시지가 나왔습니다.
b두 번째는 error 클래스를 상속 받지 않고 새로 클래스를 정의 해서 에러를 처리 하
는 방법
→ 해당 방식으로 했을 때는 조금 다른 에러가 나왔는데 이 때 발생한 에러에 대해서
구글링을 해본 결과 next에서는 next에 지정 돼 있는 특정 class들이 아닌 이상
client side로 클래스를 props로 넘길 수 없는 이슈가 있었습니다.
결국 두 가지 방식 다 next의 컴파일러에서 파싱 하는 부분에서 error 클래스일 경우 내
부에 있는 내용을 전부 제거 한뒤 위의 에러 메지시로 보내버리고
error 클래스 등 next에서 특정 해 놓은 클래스가 아니면 에러가 발생하도록 구현 해 놓
은 듯 합니다.
해결 방법
첫 번째 방식과 두 번째 방식이 전부 되지 않아서 마지막으로 시도한 부분 입니다.
첫 번째로 에러를 발생 시키는 부분에서 에러에 대한 작업이 필요한 에러들은
ServerToClientError라는 클래스를 정의 하여 fetch api를 서버에서 처리 하는 부분에서 throw를 합니다.그 이후 api를 호출하는 부분에서 JSX와 가장 근접한 부분의 서버 단에서 instanceof로 해당 에러의 catch 부분에서 에러 클래스를 throw 하는 것이 아닌 객체로 새로 만들어 return 을 합니다.→ 위와 같이 하면 error 클래스 도 아니고 next에서 지정한 특정 클래스도 아니기 때문에 next에서 막아 놓은 부분에 대해 우회 할 수 있습니다. 해당 부분으로 처리하는 것이 맞는지는 잘 모르겠지만 기존에 만들어 놓을 로직 부분들이 대부분 백엔드에서 전달 해 주는 에러 메시지에 의존을 하고 있도록 구현 돼 있어 이렇게 처리 할 수 밖에 없었는데 조금 더 깊게 공부 해봐야겠다는 생각이 많이 들었습니다.
2. Page router 와 App router
해당 프로젝트를 구현 하며 FSD아키텍쳐로 폴더 구조를 구성 하였습니다.
우선 FSD 아키텍쳐란
Layers, Slices, Segment 폴더로 구분하고 Depth를 3 으로 제한하고
app: 애플리케이션 로직이 초기화되는 곳입니다. 프로바이더, 라우터, 전역 스타일, 전역 타
processes: 이 레이어는 여러 단계로 이루어진 등록과 같이 여러 페이지에 걸쳐 있는 프로
pages: 이 레이어에는 애플리케이션의 페이지가 포함됩니다. (브라우저 주소단위의 컴포넌
widgets: 페이지에 사용되는 독립적인 UI 컴포넌트입니다.
features: 이 레이어는 비즈니스 가치를 전달하는 사용자 시나리오와 기능을 다룹니다. 예
entities: 이 레이어는 비즈니스 엔티티를 나타냅니다. 이러한 엔티티에는 사용자, 리뷰, 댓
shared: 이 레이어에는 특정 비즈니스 로직에 종속되지 않은 재사용 가능한 컴포넌트와 유
이런 식으로 폴더 구조를 구성 하는 아키텍쳐입니다.
문제점
Next.js 에서는 기존에 사용하던 page router를 deprecated 시키지 않고 app
router 기능만 추가하여 두 가지 기능을 다 사용할 수 있게 해놨습니다.
이로 인해 발생하는 문제점이 FSD의 폴더 구조에는 pages라는 폴더가 있는데 nextjs
가 개발 환경으로 실행 시킬 때는 에러가 발생하지 않지만
빌드 할 때는 pages 폴더 밑에 있는 파일들은 대부분 page 라우터라고 판단하고 page
라우터의 규칙을 적용 하다 보니 에러가 발생합니다.
이로 인해 FSD 아키텍쳐를 사용 할 때 pages라는 이름 대신 views 라는 이름으로 대
체 하여 사용 했습니다.
개발 환경을 실행 할 때는 모든 폴더를 스캔하고 컴파일 단계를 거치지 않아 app router
→ page router 순으로 선언 돼 있는 부분을 적용시켜 에러가 터지지 않습니다.
이러한 부분을 보아 next의 가장 큰 문제점은 빌드 환경과 개발환경이 일치하지 않은 것이
가장 큰 문제라고 생각합니다.
3. 환경 변수
next 에서 환경 변수를 사용 할 때는 NEXT_PUBLIC을 붙여줘야 한다고 구글링을 하면 나오는데 실제로 해당 부분에는 차이가 있습니다. 서버 사이드에서 동작하는 경우 → 환경변수를 원하는데로 만들어도 동작을 함. 클라이언트 사이드에서 동작하는 경우 → NEXT_PUBLIC을 붙여 줘야함.
4. Turbo pack
터보팩의 경우 다른 라이브러리가 없을 경우에는 성능이 좋지만 jotai등 특정한 라이브러리 들은 아직 turbopack과 호환이 잘 안돼 실행하자마자 터지는 경우가 있어 아직 사용하기에는 좀 이른 느낌이 있었습니다.
5. Date
next는 서버이다 보니 백엔드에서 자주 발생 하는 date가 UTC로 잡히는 문제가 있었습니다.
따로 서버 시간을 설정하는 방법은 찾을 수 없어서 이러한 함수를 따로 구현 하여 한국 시간으로 바꾼 뒤 백엔드로 보내는 작업을 해야 할 필요
가 있었습니다.
export const getKoreanTime = (date: Date) ⇒ {
const newDate = new Date(date);
newDate.setHours( 0 , 0 , 0 , 0 ;
const KR_TIME_DIFF 9 60 60 1000 ;
const utc = newDate.getTime();
return new Date(utc KR_TIME_DIFF;
};
6. middle ware
next에서 middle ware는 서버로 동작하며 클라이언트와 서버의 출구와 입구라고 보시면 될 것 같습니다.
여기서 사용하기 번거로웠던 점은 서버 컴포넌트에서 지금 페이지의 url 등을 확인 하기 위해서는 middle ware에서 받은 request 객체에서 url을 가져온 다음 response 객체에 헤더를 따로 생성하여 응답 메시지를 보내는 방법으로 처리를 해야하는 부분에 대해 번거로움을 많이 느꼈습니다.
const requestHeaders = new Headers(req.headers);
requestHeaders.set("x-pathname", url);
if (req.method === "POST") {
requestHeaders.set("x-action", "server");
}
return NextResponse.next({
request: {
headers: requestHeaders,
},
});