-
[Next.js] On-Demand-Revalidation 고군분투 적용기Next.js 2023. 1. 31. 22:02
이전 줄거리
상황은 이랬다
22년 8월경 우리는 react.js로 운영중인 서비스를 next.js로 대대적인 마이그레이션 작업을 진행 후 배포했으나..
SSR로 작업한 상품 상세 페이지 진입 시 로드속도가 체감될 정도로 느려진 것이다.
해결책으로 ISR이라는 정적 증분 생성 함수를 사용하여 정적 페이지 생성으로 갖는 성능상의 이점과, revalidation으로 업데이트 시에도 변경이 가능한 이점을 동시에 가지며 이 문제를 해결했다.
하지만 이 또한 완벽하진 않았는데...
문제 / 원인
문제는 바로 업데이트가 revalidation 타임에 의존한다는 것이다.
이 말은 두 가지로 풀이될 수 있다.
첫 번째로 관리자가 정보를 업데이트 하더라도 revaliation 타임만큼 기다려야 한다는 의미를 가지고
두 번째로 정보가 변경되지 않더라도 revalidation에 의해 불필요한 페이지 재생성이 될 수도 있다는 의미를 지닌다.
이로인해 관리자는 상품의 상태를 업데이트 한 후 변경된 상태를 확인하기 위해 새로고침을 30번을 갈겼다는 뒤늦은 후문(?)을 들었다.
하지만 사실상 10초 내외로 꽤 빠른 업데이트가 됐고 당장에 기능적으로 문제가 없었기 때문에 우선순위에서 조금씩 밀리게 된다.
해결방법
급한 불을 껐지만 말끔히 해결되지 않은 찝찝함으로 괜스레 한번씩 나와 같은 니즈가 있지 않나 하고 보던 중
이런 요청이 실제 많았던 것인지 Next는 v.12.1.0 부터 새로운 feature를 공개했다.
그것이 바로 On-Demand-ISR(현재는 On-Demand-Revalidation) 라는 Data Fetching 함수이다.
On-Demand-Revalidation가 어떻게 해결하는데?
위의 문제에서 살펴봤듯 ISR은 충분히 좋은 전략을 제시했지만, 업데이트가 안된(stale) 데이터를 가지고 있는 캐시된 페이지가 보여지는 것을 완전히 해결하지는 못했다. 또한 정보가 업데이트가 됐더라도 revalidation time 중간에 유저가 리프레쉬할 경우 유저는 이전의 캐시 정보를 볼 수 밖에 없는 태생적인 한계를 지닌다.
그래서 Next.js는 특정한 페이지에서 업데이트가 발생 시 revalidation 타임에 의존하지 않고, 수동 혹은 자동으로
On-Demand-Revalidation 함수를 이용해 웹훅을 보낼 수 있게 만들어 캐시 정보를 업데이트 시킬 수 있게 만들었다.즉 유저와 관리자는 revalidation 타임만큼 기다릴 필요 없이 바로 바로 fresh한 data를 볼 수 있다는 말이다.
오호라! 내가 꼭 필요했던 기능이군 얼른 써봐야지....ㅎㅎㅎ
해결내용
대략적인 과정은 아래와 같다.
1. next api route로 revalidate api 함수를 생성하여 갱신에 필요한 함수(res.revalidate())를 정의한다.
2. 어드민 페이지에서 업데이트 필요 시 1번에서 정의한 api로 프론트 서버에 호출한다.
3. 업데이트가 필요한 페이지 컴포넌트들이 갱신된다.
복잡해 보이지만 코드로 구현할 것은 정말 별로 없다import type { NextApiRequest, NextApiResponse } from 'next'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { if (req.headers.secret !== process.env.NEXT_PUBLIC_ISR_REVALIDATION_SECRET_TOKEN) { return res.status(401).json({ message: 'Invalid token' }); } const { id } = req.body; try { await res.revalidate(`/damhwaMarket/detail/${id}`); return res.json({ revalidated: true }); } catch (err) { // If there was an error, Next.js will continue // to show the last successfully generated page return res.status(500).send('Error revalidating'); } }
pages 아래 api를 만들고 그 안에 revalidate.ts라는 파일을 생성하여 갱신을 위한 api를 만든다.
(Next는 위와 같이 간편하게 백엔드 api 라우트를 만들 수 있는 전천후 프레임워크이다.)
유효한 요청인지 req header의 secret 값과 내가 가진 토큰의 값의 일치여부를 확인후
body로 받은 id값을 res.revalidate 함수의 페이지 경로에 보내면 해당 page url을 가진 페이지 컴포넌트가 갱신되는 것이다.
하지만 그렇게 쉽게 될리가...
해결할 수 있다는 기대감은 곧 실망과 좌절로 바뀌었고 마지막 즈음에는 누가 이기나 하는 집착으로까지 이어졌다.
장애물
내가 만난 장애물은 바로 CORS이다.
우선 CORS가 뭔데?
브라우저에서는 보안적인 이유로 cross-origin의 HTTP 요청을 제한한다(기본적으로 Same Origin Policy).
(모든 곳에서 데이터를 요청할 수 있다면 다른 사이트에서 원래 사이트를 흉내내여 사용자가 로그인 하도록 만들고 그 세션을 탈취하여 악의적으로 정보도 추출 가능하다. 이러한 공격을 할 수 없도록 브라우저에서 보호하고 필요한 경우에만 서버와 협의하여 요청하도록 하기위해 CORS가 필요)
그래서 cross-origin 요청을 하려면 서버의 동의가 필요한데, 동의할 때 브라우저에서는 요청을 허락하고 서버에서 동의하지 않는다면 브라우저에서는 거절한다.
이러한 허락을 구하고 거절하는 메커니즘을 HTTP-header를 이용해 가능한데 이를 CORS(Cross-Origin-Resource-Sharing) 이라고 부른다. 브라우저에서 cross-orign 요청을 안전하게 할 수 있도록 하는 메커니즘이다.
프론트엔드 개발을 하면 무조건 한 번 씨게 데인다는 무시무시한 녀석인 CORS이지만
서버의 동의를 구하면 해결할 수 있는 일이므로 보통 내가 해결할 일은 없었다(서버 담당자가 설정함).
하지만 이 경우를 보면 api 라우트로 서버의 api를 만들어 요청하는 입장이 아니라 요청받는 입장이 된 것이다.
즉 어드민 페이지 -> 프론트(쇼핑몰) 서버로 요청을 한 것인데 프론트 서버의 입장에서 origin이 다르므로 당연히 CORS에러가 뜨는 것이다.
하지만 걱정마라 Next.js에서는 친절하게 api-routes 생성시 CORS에러 어떻게 처리하는 지 알려준다(사이트 바로가기).
import type { NextApiRequest, NextApiResponse } from 'next' import Cors from 'cors' // Initializing the cors middleware // You can read more about the available options here: https://github.com/expressjs/cors#configuration-options const cors = Cors({ methods: ['POST', 'GET', 'HEAD'], }) // Helper method to wait for a middleware to execute before continuing // And to throw an error when an error happens in a middleware function runMiddleware( req: NextApiRequest, res: NextApiResponse, fn: Function ) { return new Promise((resolve, reject) => { fn(req, res, (result: any) => { if (result instanceof Error) { return reject(result) } return resolve(result) }) }) } export default async function handler( req: NextApiRequest, res: NextApiResponse ) { // Run the middleware await runMiddleware(req, res, cors) // Rest of the API logic res.json({ message: 'Hello Everyone!' }) }
api 라우트에 api/cors 파일을 저렇게 정의하면 된다.
근데 아무리 해도 되지 않는 것이 아닌가?
그때부터 Proxy를 사용해서 우회하는 방법부터 next.config에서 경로를 rewrite하는 방법, nextjs-cors 라이브러리까지 모든 방법을 동원해도 되지 않는 것이다....
실망 좌절 낙담 그리고 집착적으로 빌드하고 테스트를 했다. 백번은 더 해보것 같다.
그러다 우연히 이 글 에서 해답을 찾아 적용했더니 됐다고 이 글을 마치려고 했는데 !!!!
이 글을 작성하며 깨달았다.
아 cors 핸들러를 선언만 했지 저거를 사용하지 않았구나 결국 저 글에서 제시한 방식은 next에서 제공한 cors 에러 해결 방법과 같은 방법이구나....
개허탈... 코드는 아래와 같다.
import Cors from 'cors'; import type { NextApiRequest, NextApiResponse } from 'next'; // Initializing the cors middleware // You can read more about the available options here: https://github.com/expressjs/cors#configuration-options const cors = Cors({ methods: ['POST', 'GET', 'HEAD'], }); // Helper method to wait for a middleware to execute before continuing // And to throw an error when an error happens in a middleware function runMiddleware(req: NextApiRequest, res: NextApiResponse, fn: Function) { return new Promise((resolve, reject) => { fn(req, res, (result: any) => { if (result instanceof Error) { return reject(result); } return resolve(result); }); }); } export default async function handler(req: NextApiRequest, res: NextApiResponse) { // Run the middleware await runMiddleware(req, res, cors); const { id } = req.body; try { await res.revalidate(`/damhwaMarket/detail/${id}`); return res.json({ revalidated: true }); } catch (err) { // If there was an error, Next.js will continue // to show the last successfully generated page return res.status(500).send('Error revalidating'); } }
위와 같이 CORS 라이브러리를 설치하고 저 Rest of the API logic에 아까의 revalidate로직을 넣으면 끝이다.
부디 나와 같은 삽질을 하지 마시길 바라며 이 글을 마친다.
요약
- On-Demand-Revalidation을 사용하면 ISR의 revalitaion 타임 의존성을 제거하여 바로 업데이트가 가능하다.
- 구현 과정에서 CORS 문제로 굉장한 삽질 끝에 해결했다.
- 글을 쓰는 중간에 왜 안됐는지 깨달아서 허탈하다.
'Next.js' 카테고리의 다른 글
[Next.js 마이그레이션 2편] 나혼자 마이그레이션하다 (0) 2023.02.05 [Next.js 마이그레이션 1편] 프로젝트 도입배경 (0) 2023.02.04 [Next.js] SEO 그것이 알고 싶다(NEXT의 필요성) (0) 2023.02.03 [Next.js] ISR(Incremental Static Regeneration) 적용기 (0) 2023.01.30