[트러블슈팅] Next.js + Module not found
Next.js MDX 블로그 프로젝트에서 Module not found 오류를 해결한 과정에 대해 기록한 페이지입니다.
Tags
Troubleshooting, Typescript, Next.js, FSD
1. ✅ 개요
Next.js MDX 블로그 프로젝트에서 Module not found 오류를 해결한 과정에 대해 기록한 페이지입니다.
2. ❓ 문제
2.1. ⚠️ 오류
Tips
발생한 버그를 간략히 설명해 주세요.
다음 사진과 같이 fs 모듈을 찾을 수 없다는 Module not found 오류가 발생하였습니다.

2.2. 🖥️ 발생 환경
Tips
운영체제, 브라우저, 의존성 목록 등을 작성해 주세요.
- OS: MacOS
- Browser: Chrome
- Next.js 16.1.4
2.3. 🕘 발생 일시
Tips
버그가 발생한 날짜와 시간을 입력해 주세요. (Ex. 2024년 10월 1일, 오후 3시 30분)
- 2026년 2월 20일, 오후 4시 0분
3. 📖 해결 과정
먼저 문제가 발생하는 상황에 대해 분석하였습니다. 문제 상황을 분석한 결과 fs 모듈은 서버에서만 사용할 수 있지만, fs 모듈을 사용하는 서버 측 코드가 클라이언트 번들에 포함되어 발생한 오류였습니다.
좀 더 구체적으로 설명하자면, 먼저 다음과 같이 서버 컴포넌트인 layout.tsx 파일에서
클라이언트 컴포넌트인 SearchResultViewer를 사용하고 있었습니다.
1/* @/app/layout.tsx */
2
3import type { Metadata } from "next";
4import "./globals.css";
5import { Sidebar } from "@/widgets/sidebar";
6import localFont from "next/font/local";
7import { Header } from "@/widgets/header";
8import { RecentlyUpdatedPostList } from "@/widgets/recentlyUpdatedPostList";
9import { TrendingTagList } from "@/widgets/trendingTagList";
10import { ScrollToTopButton } from "@/features/scrollToTop";
11import { Footer } from "@/widgets/footer";
12import { SearchResultViewer } from "@/widgets/searchResultViewer";
13import { getAllPostList } from "@/entities/post";
14
15const pretendardFont = localFont({
16 src: "./PretendardVariable.woff2",
17 weight: "100 900",
18});
19
20export const metadata: Metadata = {
21 title: {
22 default: "노현진's Blog",
23 template: "%s | 노현진's Blog",
24 },
25 description: "Generated by create next app",
26};
27
28export default async function RootLayout({
29 children,
30}: Readonly<{
31 children: React.ReactNode;
32}>) {
33 const postList = await getAllPostList();
34
35 return (
36 <html
37 lang="ko"
38 className={pretendardFont.className}
39 suppressHydrationWarning
40 >
41 <head>
42 <link
43 rel="icon"
44 href="/images/icon/falling-star-logo.webp"
45 sizes="any"
46 />
47 <script
48 dangerouslySetInnerHTML={{
49 __html: `(function () {
50 try {
51 const theme = localStorage.getItem("theme");
52
53 if (theme === "dark") {
54 document.documentElement.classList.add("dark");
55 }
56 } catch (e) {}
57 })();`,
58 }}
59 />606162636465666769707172737475767778798081
82 );
83}또한 클라이언트 컴포넌트인 SearchResultViewer에서 FSD 아키텍처의 entities 레이어의 SearchResult 컴포넌트를 사용하고 있었습니다.
1/* @/widgets/searchResultViewer/ui/SearchResultViewer.tsx */
2
3"use client";
4
5import { SearchResult } from "@/entities/post";
6import { useSearchInputStore } from "@/features/searchPostListByTitle";
7import { useEffect, useEffectEvent, useState } from "react";
8
9interface SearchResultViewerProps {
10 postList: {
11 title: string;
12 description: string;
13 category: string;
14 tagList: string[];
15 postPath: string;
16 }[];
17}
18
19export const SearchResultViewer = ({ postList }: SearchResultViewerProps) => {
20 const [searchResultList, setSearchResultList] = useState<
21 {
22 title: string;
23 description: string;
24 category: string;
25 tagList: string[];
26 postPath: string;
27 }[]
28 >([]);
29
30 const updateSearchResultList = useEffectEvent((input: string) => {
31 setSearchResultList(
32 postList.filter((post) =>
33 post.title
34 .replaceAll(" ", "")
35 .toLowerCase()
36 .includes(input.replaceAll(" ", "").toLowerCase()),
37 ),
38 );
39 });
40
41 const { input } = useSearchInputStore();
42
43 useEffect(() => {
44 updateSearchResultList(input);
45 }, [input]);
46
47 return (
48 <div className="w-full px-6">
49 {searchResultList.length === 0 ? (
50 <div className="mt-24 flex items-center justify-center">
51 <p className="text-custom-gray">검색 결과가 없습니다.</p>
52 </div>
53 ) : (
54 <div className="grid grid-cols-2 gap-x-12 gap-y-4">
55 {searchResultList.map((post) => (
56 <SearchResult
57 key={post.postPath}
58 title={post.title}
59 description={post.description}
60 category={post.category}
61 tagList={post.tagList}
62 postPath={post.postPath}
63 />
64 ))}
65 </div>
66 )}
67 </div>
68 );
69};1/* @/entities/ui/SearchResult.tsx */
2
3import Link from "next/link";
4import { FaRegFolder } from "@react-icons/all-files/fa/FaRegFolder";
5import { FaTag } from "@react-icons/all-files/fa/FaTag";
6
7interface SearchResultProps {
8 title: string;
9 description: string;
10 category: string;
11 tagList: string[];
12 postPath: string;
13}
14
15export const SearchResult = ({
16 title,
17 description,
18 category,
19 tagList,
20 postPath,
21}: SearchResultProps) => {
22 return (
23 <article className="mt-12 flex flex-col gap-1">
24 <header className="flex flex-col gap-2">
25 <h2>
26 <Link
27 className="text-custom-blue text-2xl font-semibold underline-offset-4 hover:text-teal-500 hover:underline"
28 href={`/posts/${postPath}`}
29 >
30 {title}
31 </Link>
32 </h2>
33 <div className="text-custom-gray flex flex-row items-center gap-6 text-sm">
34 <div className="flex flex-row items-center gap-1">
35 <FaRegFolder className="text-xs" />
36 {category}
37 </div>
38 <div className="flex flex-row items-center gap-1">
39 <FaTag className="text-xs" />
40 {tagList.join(", ")}
41 </div>
42 </div>
43 </header>
44 <p className="text-custom-gray">{description}</p>
45 </article>
46 );
47};위의 코드를 보면 알 수 있듯이, SearchResult 컴포넌트에서는 fs 모듈을 사용하고 있지 않지만,
SearchResultViewer와 SearchResult 두 컴포넌트는 레이어가 서로 다르며 @/entities/post/index.ts 파일을 통해
SearchResult 컴포넌트를 가져오고 있습니다. 이 때, index.ts 파일은 아래와 같이 fs 모듈을 사용하는 getAllPostList.ts 파일을 포함하고 있습니다.
1/* @entities/post/index.ts */
2
3export { getArchiveList } from "./model/getArchiveList";
4export { getCategoryList } from "./model/getCategoryList";
5export { getAllPostList } from "./model/getAllPostList";
6export { getPostListByCategory } from "./model/getPostListByCategory";
7export { getPostListByTag } from "./model/getPostListByTag";
8export { getRecentlyUpdatedPostList } from "./model/getRecentlyUpdatedPostList";
9export { getTagList } from "./model/getTagList";
10export { getTrendingTagList } from "./model/getTrendingTagList";
11export { type PostMetadata } from "./model/postMetadata";
12
13export { SearchResult } from "./ui/SearchResult";
14export { CategoryCard } from "./ui/CategoryCard";
15export { PostCard } from "./ui/PostCard";1/* @/entities/post/model/getAllPostList.ts */
2
3import fs from "fs";
4import path from "path";
5import { PostMetadata } from "./postMetadata";
6
7const postsDirectory = path.join(process.cwd(), "src/content");
8
9export const getAllPostList = async () => {
10 const fileList = fs.readdirSync(postsDirectory);
11
12 const postList = await Promise.all(
13 fileList.reverse().map(async (file) => {
14 const mod = await import(`@/content/${file}`);
15 const metadata: PostMetadata = mod.metadata;
16
17 return {
18 title: metadata.title,
19 description: metadata.description,
20 date: metadata.date,
21 category: metadata.category,
22 imagePath: metadata.imagePath,
23 tagList: metadata.tags,
24 postPath: file.split(".")[0],
25 };
26 }),
27 );
28
29 return postList;
30};
정리하자면, 클라이언트 컴포넌트인 SearchResultViewer 컴포넌트에서 SearchResult 컴포넌트를 가져오기 위해 index.ts를 import할 때
fs 모듈을 사용하는 서버 전용 파일까지 함께 가져와서 발생한 문제였습니다.
1SearchResultViewer (Client)
2 ↓
3entities/post/index.ts
4 ↓
5getAllPostList.ts (fs 사용)즉, 이 문제를 해결하기 위한 핵심은 서버 측 코드가 클라이언트 번들에 포함되지 않도록 분리하는 것이었습니다.
따라서 SearchResult 컴포넌트를 import할 때 서버 측 코드가 클라이언트 번들에 포함되지 않도록
SearchResult.tsx 파일을 SearchResultViewer.tsx 파일과 같은 폴더 내에 위치시키면 문제를 해결할 수 있습니다.
위와 같이 문제를 해결한 결과는 다음과 같습니다.


4. 📚 참고 자료
- Module Not Found | Next.js
- Next.js에서 fs모듈 사용하기 :: ModuleNotFoundError: Module not found: Error: Can't resolve 'fs' in
