📌 목차
- 서론
- NextServerLogger 그게 뭔데?
- NextServerLogger 설계
- NextServerLogger 간단한 구현
- Logger.log
- NextServerProvider
- 결론
📌 서론
혹시 그러신 적 있나요? Next.js 프로젝트로 배포는 했는데... 분명 로컬에서는 잘 동작했는데... 수많은 유닛 테스트, e2e 테스트들도 통과했는데.. 배포를 했더니.. Next.js 프로젝트가 뻗어 버린 적 있나요?
네, 저는 있습니다! (당당😎)
getInitialProps, getServerSideProps, getStaticProps 내부 코드에서 터지는거 같은데 이걸 어떻게 디버깅을 하지? 고민하신 적 있으신가요? 방법은 있습니다. docker 환경이라면 해당 container 내부에 들어가서 log를 찍는 방법이요. 그리고 아마 다른 방법들도 있을 거예요.
근데 만약에 그 방법이 어렵다면 후임자 혹은 다른 동료에게 제가 부재시 어떻게 할까요? 문서화를 해 논다! 좋은 방법입니다. 근데 만약에 부서가 다르고 각 부서의 권한을 획득해야 한다면... 더 복잡한 과정을 거쳐야 한다면... 어떡할까요? 프론트 엔드 코드로는 해결을 못 할까요? NextServerLogger는 바로 이 물음에 답변을 하기 위해 만들어졌습니다.
더 이상 등에 식은땀을 그만 흘리고 싶었어요. 😭
📌 NextServerLogger 그게 뭔데?
간단하게 말하면 Next.js앱에서 ssr을 위해 사용하는 getInitialProps, getServerSideProps, getStaticProps를 브라우저 로그에 찍어주는 Next Library입니다. 그리고 이런식으로 동작을 하게 되어 있어요.
동작하는 모습 |
---|
네, 이제 getInitialProps, getServerSideProps, getStaticProps 모두 브라우저 콘솔을 통해 확인할 수 있어요. 직접 서버로 들어가서 로그를 보지 않고도 어떻게 브라우저 콘솔로 찍을 수 있었을까요? 정답은 NextContext.pageProps를 활용하면 됩니다.
📌 NextServerLogger 설계
NextServerLogger를 설명하기 전에 Next에서 pageProps를 간단하게 알아볼게요.
User는 NextServer에게 page 요청을 하게 됩니다. 그럼 Next는 서버에서 getInitialProps, getServerSideProps, getStaticProps를 실행하고 HTML 만들게 되어있어요. 그리고 그 HTML을 요청자에게 넘겨줍니다. 그런 뒤에 javascript 파일들을 실행시켜 우리가 알고 있는 SPA(React App)으로 변환하기 위해 hydration이라는 과정을 거치게 됩니다. 이때 server side에서는 HTML을 만들기 위해 pageProps를 넘겨받게 됩니다. Client side에서는 hydration을 위해 해당 데이터를 받게 되어 있어요.
그렇다면 server side에서 찍혀진 로그를 받아서 array로 저장을 하고 그걸 hydration이 일어날 때 로그를 찍어준다면 우리가 의도한 대로 server에서 찍힌 로그를 받아볼 수 있게 됩니다.
📌 NextServerLogger 간단한 구현
위의 대략적인 개념에 조금 더 확장을 시켜볼게요. 첫번째 단계인 서버에서 getInitialProps, getServerSideProps, getStaticProps data fetching을 하는 부분부터 살펴볼게요.
먼저 우리는 이런 형태의 코드를 만들어야 해요.
export const getServerSideProps = async () => {
logger.log('hello eddie')
}
그리고 앞써 말한 대로 client에서 hydration(browser phase)을 위해 pageProps를 활용한다고 했으니 pageProps를 넘겨주면 이런 코드가 탄생하겠지요.
export const getServerSideProps = async () => {
logger.log('hello eddie')
return {
props: {
// logs 키값은 내가 알아볼 수 있는 키 값으로 정의
logs: ['hello eddie'] // ⭐️ 위에서 hello eddie를 찍었으니 array에는 hello eddie가 담겨있어야 함.
}
}
}
그리고 이제 hydration 시점에서 pageProps로 담긴 어레이를 순회해서 찍어주기만 하면 됩니다.
// _app
import React, { useEffect } from 'react'
export default function App({ Component, pageProps }) {
// useEffect에 정의 된 callback은 mount 되는 시점에 불리기 때문에 client side에서만 실행(= hydration 시점)
useEffect(() => {
// 위에서 던져준(getServerSideProps) logs는 pageProps에 담겨져 내려옴
pageProps.logs.forEach((log) => console.log(log))
}, [])
// ...
}
네, 이게 끝입니다. 참 쉽죠? 😎 이렇게 하면 이제 우리는 getServerSideProps, getInitialProps, getStaticProps 모두 무섭지 않아요.
이제 이 코드들을 가지고 한번 library 형태로 만들어볼게요.
📌 Logger.log
logger.log 인자 값들을 array 형태로 담고 있기만 하면 됩니다. 그래서 logger를 class 형태로 만들게 되었어요.
import { stringify } from 'flatted'
class NextServerLogger {
loggerStack = []
log(...msg) {
this.loggerStack.push(stringify(msg))
}
}
export const nextServerLogger = new NextServerLogger()
🚧 flatted.strigify는 왜 사용했어요?
pageProps에 던져주려면 object 형태는 던져주지 못해요.(next 내부 구현) 그래서 strigify를 시켜주었습니다.
그럼 이제 getServerSideProps에서 사용할땐 이렇게 사용이 가능하겠죠?
export const getServerSideProps = async () => {
logger.log('hello eddie')
return {
props: {
logs: logger.loggerStack
}
}
}
근데 지금 위의 코드에는 두가지 문제점이 있어요.
- props의 logs 라는 키 값이 만약에 사용자에 의해 중복이 된다면? + 어떤 키 값으로 stack을 던져줘야 하는지 모르겠음.
- single-ton으로 되어있기 때문에 모든 요청이 logger.loggerStack에 쌓이게 됨.
그래서 이런 코드가 이렇게 리팩토링이 되었어요.
import { stringify } from 'flatted'
class NextServerLogger {
loggerStack = []
log(...msg) {
this.loggerStack.push(stringify(msg))
}
reset() {
this.loggerStack = []
}
getLoggerProps() {
const prev = [...this.loggerStack];
this.reset(); // ⭐️ 한번 pageProps로 전달해준 stack은 초기화를 시켜준다 -> 2번 문제 해결
return {
[SERVER_LOGGER_PROPS_KEY]: prev, // ⭐️ NextServerLogger에서만 사용하는 props 키값을 설정한다 -> 1번 문제 해결
};
}
export const nextServerLogger = new NextServerLogger()
그럼 이제 이렇게 코드를 작성할 수 있겠죠?
export const getServerSideProps = async () => {
logger.log('hello eddie')
return {
props: {
...logger.getLoggerProps()
}
}
}
📌 NextServerProvider
hydration 시점에서 로그를 찍어 주는 코드를 다시 한번 볼까요?
// _app
import React, { useEffect } from 'react'
export default function App({ Component, pageProps }) {
useEffect(() => {
pageProps.logs.forEach((log) => console.log(log))
}, [])
// ...
}
이 코드에도 마찬가지로 두가지 문제점이 있어요.
- logger의 특성상 특정 환경에서는 로그를 찍고 싶지 않을 수 있어요.
- pageProps는 SPA의 특성상 페이지 이동에도 동일하게 계속 내려오게 됩니다.
그래서 이렇게 변경해줬습니다.
import { parse } from 'flatten'
function NextServerLoggerProvider({ pageProps, enable = true }) {
const serverLogger useMemo(() => nextServerLogger, [])
useEffect(() => {
const loggerServerStack = pageProps?.[SERVER_LOGGER_PROPS_KEY] ?? []
loggerServerStack.forEach((log) => {
if (!enable) return /* guard */ // enable 유무로 로그를 찍지 않는다 -> 1번 문제 해결
console.log(parse(log)) // 넘겨 줄때 stringify로 넘겨 주었기 때문에 다시 parse를 한다
})
if (loggerServerStack) delete pageProps[SERVER_LOGGER_PROPS_KEY] // 다 사용한 props는 지워준다. -> 2번 문제 해결
}, [pageProps, enable])
return <></>
}
export default function App({ pageProps, Component }) {
return (
<>
<NextServerLogger pageProps={pageProps} enable={process.env.NODE_ENV !== 'production'} />
// ...
</>
)
}
📌 결론
간단하게 NextServerLogger 구현에 대해 알아보았습니다. 핵심적인 내용만 설명하느라 실제 NextServerLogger 몇 가지 기능이 빠져있어요. 혹시라도 풀 코드가 궁금하시다면, NextServerLogger Github로 들어가셔서 보시거나 시간이 없으셔서 바로 사용하고 싶으시다면 package 설치하시면 됩니다.🙇🏻♂️
스타도 한번 꾸욱 눌러주세요! ⭐️
등에 흘렀던 식은땀에게 이 글을 바칩니다.
'회사이야기' 카테고리의 다른 글
회사 이야기 - Visual Regression Test feat. Cypress (0) | 2022.07.23 |
---|