next.js 로 블로그를 구축하다보면 가장 크게 느껴지는 문제는 글 작성시의 UX입니다.
사실상 이 문제 때문에 저도 자체 구축한 블로그를 거의 작성하지 않고 있었다고 해도 무방한데요
Dan Abramov의 블로그 github에서 해당 문제를 해결한 것을 확인하고 저도 PR을 분석하여 적용하였습니다.
https://github.com/gaearon/overreacted.io/pull/797/files
사실 이 문제는 nextjs에서 mdx 뿐만 아니라 contents 전반에 깔려있는 문제이기도 합니다.
다만 실시간으로 변경사항을 확인해야하는 경우의 대표적인 사례가 mdx이다보니 이부분에 초점이 맞추어 진 것 뿐이라고도 할 수 있겠네요
FashRefresh 문제를 해결하는 방법
Dan의 위 PR의 경우에는 chokidar , npm-run-all , ws 를 통해 핫리로드를 트리거할 방법을 구축합니다.
각 라이브러리의 역할을 나열하면 다음과 같습니다.
ws | 알다시피 웹소켓이에요 |
npm-run-all | N개의 스크립트를 동시에 실행할 수 있게 도와줍니다. |
chokidar | file watch 기능을 지원하는 라이브러리입니다. |
각 라이브러리들의 역할을 보면 어떤 것을 하려고 하는지 어느정도 예상이 되는데요 웹소켓 서버를 켜두는걸 통해 개발환경에서 특정 변경사항에 대해 지속적으로 listen하면서 chokidar을 통해 파일이 변경되었는지를 감시하는 것입니다.
그런데 이를 위해서는 개발환경에서 next 서버와 함께 웹소켓 서버도 실행해야하는데 이 경우 매번 터미널을 여러개 열어서 next서버도 실행시키고 웹소켓 서버도 실행시키는게 매우 귀찮으니 스크립트를 동시에 실행시킬 수 있게 해주는 npm-run-all을 통해 개발서버와 웹소켓서버가 동시에 켜질 수 있게 설정하는 것입니다.
이렇게 대충 구현이 이해가 되니까 따라하기도 쉬울거에요
Dan의 구현을 참고하면서 제 프로젝트에서도 동작할 수 있도록 커스터마이징을 약간 수행했습니다.
예컨대 Dan은 public 폴더안에 포스팅을 하지만 저는 public에서 포스팅을 하지 않고 Dan은 js로만 프로젝트를 구성했지만 저는 ts를 사용합니다.
yarn add -D ws chokidar npm-run-all
필수적인 종속성들을 설치해주고나서 프로젝트 루트에 watcher.cjs를 생성해주겠습니다.
/* eslint-disable @typescript-eslint/no-var-requires */
const { WebSocketServer } = require("ws");
const chokidar = require("chokidar");
const wss = new WebSocketServer({ port: 3001 });
const watchCallbacks = [];
chokidar.watch("./posts").on("all", (event) => {
if (event === "change") {
watchCallbacks.forEach((cb) => cb());
}
});
wss.on("connection", function connection(ws) {
ws.on("error", console.error);
watchCallbacks.push(onChange);
ws.on("close", function close() {
const index = watchCallbacks.findIndex(onChange);
watchCallbacks.splice(index, 1);
});
function onChange() {
ws.send("refresh");
}
});
chokidar.watch 부분에 자신이 와칭하고싶은 폴더의 경로를 지정해주면 되겠습니다.
"use client";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
/* eslint-disable no-func-assign */
function AutoRefresh({ children }) {
return children;
}
if (process.env.NODE_ENV === "development") {
AutoRefresh = function AutoRefresh({ children }) {
const router = useRouter();
useEffect(() => {
console.log("is work");
const ws = new WebSocket("ws://localhost:3001");
ws.onmessage = (event) => {
if (event.data === "refresh") {
router.refresh();
}
};
return () => {
ws.close();
};
}, [router]);
return children;
};
}
export default AutoRefresh;
그리고 자신이 원하는 곳에 다음과 같은 형태의 컴포넌트를 생성합니다.
dev 환경일때에만 동작하는 형태로 작성하는 것이죠
export default function RootLayout({
children
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<AutoRefresh>
<html lang="en">
<body className={`${XION_NEXT_FONT.className} `}>
<Providers>
<Layout>{children}</Layout>
</Providers>
<Analytics />
<GoogleTagManager gtmId={ENV.gtmId} />
<GoogleAnalytics gaId={ENV.gaId} />
</body>
</html>
</AutoRefresh>
);
}
이제 프로젝트의 layout에서 다음과 같이 리프레쉬 컴포넌트로 래핑을 수행해주면 사전작업은 끝입니다.
"scripts": {
"dev": "run-p next-dev watch-content",
"build": "next build",
"start": "next start",
"lint": "next lint",
"next-dev": "next dev",
"watch-content": "node ./watcher.cjs"
},
다음과 같이 package.json의 dev 스크립트를 수정해주고 실행하면 정말 끝이에요
마치며
막상 답을 알고보면 굉장히 make sense하고 당연한 것처럼 느껴지는 구현입니다.
실제로 저도 이 문제를 고민하면서 웹소켓 서버를 통해 개발할때만 특정 파일이 변경될때마다 리프레쉬 시키면 되지않나..?
라는 생각을 하기도 하였는데요 막상 구현으로 옮기는 것에는 약간의 어려움이 있었습니다.
예를 들면 저는 npm-run-all , chokidar와 같은 라이브러리의 존재조차도 해당 PR을 분석하면서 알게되었거든요
또 사실은 혹시 nextjs에서 특정 폴더를 와칭하는 기능을 config 형태로 지원해주지 않을까?
하는 근거없는 기대로 nextjs 문서들을 살펴보는데에 시간을 많이 사용하기도 했고요
자신에게 필요한 것이 있다면 재료들을 조합하여 만들줄 아는 문제해결능력은 개발자에게 정말 중요한 것 같다는 생각을 하며 이번 글을 마치도록 하겠습니다.
읽어주셔서 감사합니다.
'frontend' 카테고리의 다른 글
프론트엔드 테스트 환경 설정하기 (0) | 2024.05.22 |
---|---|
내가 쓰는 프론트엔드 코딩 컨벤션과 네이밍 컨벤션 폴더구조 (0) | 2024.05.10 |
리액트 컴포넌트 라이브러리를 빌드하고 배포하는 방법 (5) | 2024.03.17 |
늦은 밤에 도메인 죽은 썰 푼다 (1) | 2024.01.19 |
짧은 zed editor 사용후기 (2) | 2024.01.15 |