THE DEVLOG

scribbly.

Next.js 블로그 프로젝트

2025.05.21 11:39:15

1.00

11일차부터 개발한 사이드바와 관련하여 정리한 게시글

사이드바의 리렌더링을 줄이는 Next.js의 Segement-based렌더링과 dnd-kit을 dynamic import 할 수 있도록 React의 Render-props 테크닉을 이용한 내용을 정리하였다.

Segement-based 렌더링

layout.tsx에서 사이드바를 구현하고, page.tsx에서 게시글 렌더링 한다.

app/
├── post/
│   └── [slug]/
│       ├── layout.tsx
│       └── page.tsx

이때 폴더 구조를 위와 같이 렌더링하면 게시글을 이동할 때마다 layout이 매번 리렌더링되는 이슈가 발생한다.

반면 아래와 같이 layout을 상위 폴더로 빼면 layout은 그대로 유지되고, page 부분만 RSC Payload로 주입된다.

app/
├── post/
│   ├── layout.tsx
│   └── [slug]/
│       └── page.tsx

 

이는 Next.js App Router의 segment-based의 원리 때문이다.

Routing in the App Router is segment-based and uses the file system as the router.

post/layout.tsx는 하위 세그먼트인 post/[slug] 폴더의 컴포넌트들에도 공유되고,

이에 따라 페이지를 이동하더라도 post/layout.tsx의 렌더링은 유지된다.

 

hydrator

위와 같은 원리를 이용하여 렌더링 할 때에 문제는 slug가 변경된 것을 layout.tsx가 감지하지 못한다는 것이다.

그래서 props.params를 받아 이를 사이드바에 반영하는 클라이언트 컴포넌트를 제작하였다.

main/components/post/sidebar/sidebar-hydrator.tsx

"use client";

export default function SidebarHydrator() {
  // params 가져오기
  const params = useParams();
  const rawSlug = params?.urlSlug;
  const urlSlug =
    typeof rawSlug === "string" ? decodeURIComponent(rawSlug) : "";

  // params로 게시글 찾기
  const {
    postByUrl,
  } = useSidebarStore(
    useShallow((state) => ({
      postByUrl: state.posts?.find(
        (post) => post.url_slug === decodeURIComponent(urlSlug)
      ),
    }))
  );

  // params로 찾은 게시글의 상태를 사이드바에 반영하기
  useEffect(() => {
    if (postByUrl) {
      setPost(postByUrl.id);
      for (const category of categories) {...}
    } else {
      setCategory(selectedCategoryId);
    }
    setLoaded();
  }, [
    postByUrl,
  ]);

  return null;
}

이렇게 하면 게시글 페이지 내에서 다른 게시글 페이지로 이동하더라도 slug가 바뀐 걸 감지하고 sidebar의 상태를 변경시킨다.

 

드래그 앤 드롭 구현

dnd-kit은 확장성이 좋고 모듈화가 잘 되어 있는 드래그 앤 드롭 라이브러리이다.

문제는

  1. 모듈화가 잘 되어 있어도 이 블로그에서 사용하고자 하는 플러그인이 많아 용량이 큰 편이다.
  2. dnd-kitref를 기반으로 작동하기 때문에 서버사이드 렌더링이 불가능하다.

이와 같은 문제를 해결하기 위해 기본적으로는 dnd-kit을 import하지 않고 렌더링을 하고, 편집모드 flag가 켜졌을 때에 dynamic import하도록 구현하였다.

main/components/post/sortable-list/with-sortable-list.tsx

"use client";

import dynamic from "next/dynamic";
import { useShallow } from "zustand/react/shallow";
import { SortableItem } from "@/components/post/sortable-list/sortable-list-container";
import { useLayoutStore } from "@/providers/layout-store-provider";
import { OnUpdateFn } from "@/hooks/use-order-update-queue";

export const SortableListContainerDynamic = dynamic(() =>
  import("@/components/post/sortable-list/sortable-list-container").then(
    (mod) => mod.SortableListContainer
  )
);

type Props<T extends SortableItem> = {
  items: T[];
  children: (items: T[]) => React.ReactNode;
  onUpdate: OnUpdateFn<{ id: string; order: number }>;
};

export function WithSortableList<T extends SortableItem>({
  items,
  children,
  onUpdate,
}: Props<T>) {
  const isSortable = useLayoutStore(useShallow((s) => s.isSortable));

  if (isSortable) {
    return (
      <SortableListContainerDynamic items={items} onUpdate={onUpdate}>
        {children as (items: SortableItem[]) => React.ReactNode}
      </SortableListContainerDynamic>
    );
  }

  return <>{children(items)}</>;
}

WithSortableList는 게시글을 감싸는 HOC 이다.

편집모드가 켜져있으면 dnd-kit을 import하고 적용한다.

편집 모드가 없는 경우에는 static하게 렌더링한다.

 

이때 children을 함수 형태로 받는데, 이를 Render Props라 한다.

"A technique for sharing code between React components using a prop whose value is a function."
React 공식 문서

 

이를 이용해 아래와 같이 map 함수를 children으로 받게 된다.

                <WithSortableList
                  items={categories}
                  onUpdate={(items) =>
                    updateOrders({ mode: "categories", data: items })
                  }
                >
                  {(categories) =>
                    categories.map((cat) => (
                      <SidebarCategoryContent
                        key={cat.id}
                        catagory={cat}
                        setSidebarRightCollapsed={setSidebarRightCollapsed}
                        setSubcategory={setSubcategory}
                        selectedSubcategoryId={selectedSubcategoryId}
                      />
                    ))
                  }
                </WithSortableList>