쿼리스트링, Link와서버 액션에 대하여

쿼리스트링 : 단순한 URL이 아닌 '페이지의 상태'를 담는 그릇

URL 뒤에 붙는 ?key=value 형태의 문자열인 쿼리스트링(Query String)은 단순히 서버에 데이터를 전달하는 것이 아닌 페이지의 현재 상태를 정의하는 명세서입니다.

  • /board?page=2 → "게시판의 2페이지 상태"
  • /products?category=food&sort=popular → "식품 카테고리에서 인기순으로 정렬된 상태"

쿼리스트링을 활용하면 사용자는 특정 상태의 페이지를 북마크하거나 다른 사람에게 공유가 가능합니다.
예를 들어, 내가 필터링한 쇼핑몰 검색 결과를 친구에게 링크 하나로 전달해 줄 수 있습니다.

& (AND) 연산자와 OR 표현

  • &는 여러 조건을 동시에 만족(AND)시킬 때 사용합니다. category=food 그리고 sort=popular.
  • OR 조건은 보통 같은 키를 반복해서 사용합니다. color=red&color=blue는 빨간색이거나 파란색인 상품을 찾는 요청입니다.

쿼리스트링은 서버와 클라이언트가 "페이지의 어떤 상태를 보여줄지"에 대해 소통하는 약속입니다.

<Link> 와 <a>

Next.js에서 사용할 수 있는 페이지 이동 방법입니다.  <a> 태그 대신 <Link> 컴포넌트를 쓰는 이유에 대한 표 입니다.

구분 Next.js <Link> HTML <a> 태그
핵심 클라이언트 사이드 내비게이션 서버 사이드 내비게이션
동작 페이지 전체를 새로고침하지 않고, 바뀌는 부분만 교체 서버에 새 페이지를 요청하여 문서 전체를 새로고침
경험 앱처럼 부드럽고 빠름 웹페이지가 깜빡이며 로딩됨

<Link>는 새로고침 없이 새로고칩니다. 사용자가 <Link>를 클릭하면, Next.js는 이미 브라우저에 있는 자바스크립트를 사용해 다음 페이지의 필요한 부분만 가져와 교체합니다. 사용자는 훨씬 빠르고 쾌적한 경험을 하게 됩니다.

그러나, <a> 태그는 전통적인 방식으로 서버에 새로운 HTML 문서를 요청하고 페이지 전체를 다시 그립니다. 외부 사이트로 연결할 때가 아니라면, Next.js 프로젝트 내부에서의 페이지 이동은 항상 <Link>를 사용하는 것이 정답입니다. 새로고침은 사용자 경험을 해치므로 <Link>를 지향해야 합니다.

서버 액션 : <form>의 변경, API가 필요 없는 데이터 제출

전통적으로 우리는 <form>으로 데이터를 제출하기 위해 별도의 API 엔드포인트(/api/create-post)를 만들고, fetch로 요청을 보내는 복잡한 과정을 거쳤으나, Next.js의 서버 액션(Server Actions)은 이 모든 것을 한 번에 해결합니다.

서버 액션

클라이언트 컴포넌트에서 서버의 함수를 마치 내 함수처럼 직접 호출하게 해주는 기술입니다.

기존 방식:
Form (Client) → fetch → API Route (Server) → DB

서버 액션 방식:
Form (Client) → Server Function (Server) → DB

중간의 API 라우트와 fetch 가 대체되었습니다. 코드는 더 단순해집니다.

// components/PostForm.tsx
// 'use client'

import { createPost } from '@/app/actions'; // 서버 액션 함수 임포트

export function PostForm() {
  return (
    <form action={createPost}> // action에 함수를 직접 바인딩
      <input type="text" name="title" />
      <textarea name="content" />
      <button type="submit">글쓰기</button>
    </form>
  );
}
// app/actions.ts

'use server'; // 이 파일의 함수들은 서버에서만 실행됨

import { redirect } from 'next/navigation';
import { prisma } from '@/lib/prisma';

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;
  const content = formData.get('content') as string;

  // DB에 데이터 저장
  await prisma.post.create({ data: { title, content } });

  // 작업 완료 후 게시판 페이지로 이동
  redirect('/board');
}

<form>action에 URL 대신 함수를 직접 넘겨주면 Next.js가 나머지를 알아서 처리합니다. 페이지 새로고침 없이 데이터를 제출하고 작업이 끝나면 redirect를 통해 다른 페이지로 부드럽게 이동합니다. method="POST" 같은 속성도 사용하지 않습니다.

이는 기존에 알던 API의 POST와는 다른 Next.js에서 사용하는 새로운 데이터 제출 방식입니다.

정리

오늘 정리한 세 가지 개념은 Next.js가 개발자 경험(DX)과 사용자 경험(UX)을 동시에 향상시키는 것을 보여줍니다.

쿼리스트링으로 페이지의 상태를 명확히 하고, <Link>로 부드러운 페이지 전환을 구현하며, 서버 액션으로 프론트엔드와 백엔드의 경계가 허물어집니다.

이 개념들을 이해하고 활용하면 웹 어플리캐이션 설계에 매우 유리해집니다.

728x90

팀 프로젝트에서 Konva.js를 사용하여 사용자가 직접 객체를 편집하는 인터랙티브 UI를 구현했습니다.

실제로 작성한 코드를 바탕으로, 두 가지 핵심 기능인 '드래그 앤 드롭으로 객체 생성'과 'Transformer로 객체 변형하기'를 구현한 방법을  공유하는 글을 작성합니다.


드래그 앤 드롭으로 객체 생성하기

이미지를 캔버스 위로 드래그 앤 드롭하면 마우스를 놓은 위치에 새로운 사각형이 나타나야 합니다.

브라우저가 제공하는 마우스 좌표(clientX, clientY)는 전체 화면 기준입니다. 이 좌표를 Konva 캔버스 내부의 로컬 좌표로 변환해야만 정확한 위치에 객체를 생성해야 합니다.

// EditRoomPage.tsx
const stageRef = React.useRef<Konva.Stage | null>(null);

const handleDrop = (e: React.DragEvent) => {
  e.preventDefault();

  if (!stageRef.current) return;
  const stage = stageRef.current.getStage();

  // 1. 캔버스 DOM 요소의 화면상 위치 정보 가져오기
  const stageBox = stage.container().getBoundingClientRect();

  // 2. 브라우저 좌표를 캔버스 내부 좌표로 변환
  const dropX = e.clientX - stageBox.left;
  const dropY = e.clientY - stageBox.top;

  // 3. 변환된 좌표로 새로운 객체 생성
  const newRoom: RoomDto = {
    id: `new_${Date.now()}`,
    positionX: dropX,
    positionY: dropY,
    width: 120,
    height: 100,
    name: '새로운 방',
    detail: '',
  };
  setRooms((prev) => [...prev, newRoom]);
};

 

  1. stage.container().getBoundingClientRect(): 이 함수는 Konva 캔버스를 감싸는 <div> 컨테이너의 화면 내 위치 정보(시작 left, top 좌표 등)를 가져옵니다. 이는 좌표 변환의 기준점이 됩니다.
  2. e.clientX - stageBox.left: 이 코드는 좌표 변환의 핵심입니다. 브라우저 왼쪽 끝에서 마우스까지의 거리(e.clientX)에서, 브라우저 왼쪽 끝에서 캔버스까지의 거리(stageBox.left)를 빼면 캔버스 내부에의  X좌표가 계산됩니다.
  3. setRooms(...): 계산된 dropX, dropYpositionX, positionY로 사용하여 새로운 방 객체를 만들고, React의 useState를 통해 rooms 배열 상태를 업데이트하여 화면에 렌더링합니다.

Transformer로 객체 크기 및 위치 조절하기

사용자가 사각형을 클릭하면 크기 조절 핸들이 나타나고, 핸들을 조작하여 변경한 크기와 위치가 화면에 나타나야 합니다.

Konva의 Transformer는 객체의 크기와 위치를 변경합니다. 이 변경 사항을 onTransformEnd 이벤트가 끝나는 시점에 찾아내어 rooms 배열과 동기화 합니다

// EditRoomPage.tsx
const handleTransformEnd = (room: RoomDto, node: Konva.Rect) => {
  // 1. Transformer가 변경한 scale 값을 가져온다.
  const scaleX = node.scaleX();
  const scaleY = node.scaleY();

  // 2. 새로운 너비와 높이를 계산하고, 변경된 위치도 가져온다.
  const updatedRoom: RoomDto = {
    ...room,
    positionX: node.x(),
    positionY: node.y(),
    width: Math.max(30, node.width() * scaleX), // 최소 크기
    height: Math.max(30, node.height() * scaleY),
  };

  // 3. 다음 변형을 위해, 노드의 시각적 scale을 1로 초기화한다.
  node.scaleX(1);
  node.scaleY(1);
  
  // 4. 계산된 최종 값으로 React 상태를 업데이트한다.
  setRooms((prev) => {
    const updatedRooms = prev.map((r) =>
      r.id === room.id ? updatedRoom : r
    );
    return updatedRooms;
  });
};
  1. node.scaleX(), node.scaleY() : onTransformEnd의 콜백 인자로 들어온 node는 변형이 적용된 Konva 객체입니다. 이 객체의 scale 속성을 통해 얼마나 크기가 변했는지 알 수 있습니다.
  2. node.width() * scaleX : Transformer는 객체의 width 자체를 바꾸는 것이 아니라 scale을 조절합니다. 따라서 최종 너비는 기존 너비 * 스케일 값으로 직접 계산해주어야 합니다. node.x()node.y()를 통해 변경된 최종 위치도 가져옵니다.
  3. node.scaleX(1) :  상태 업데이트 후 Konva 노드의 scale 값을 다시 1로 초기화해야 합니다. 그렇지 않으면 다음 크기 조절 시, 이전 scale 값에 새로운 scale이 곱해져서 원치 않은 결과가 나타납니다. 시각적 표현은 React 상태에 맡기고, Konva 노드의 scale은 항상 1로 유지해야 합니다.
  4. setRooms(...): 계산된 updatedRoom 객체로 rooms 배열에서 해당 ID를 가진 요소를 찾아 교체한 후, 새로운 배열로 상태를 업데이트합니다.

정리

위의 구현을 통하여 라이브러리가 제공하는 편리한 기능의 내부 동작 원리를 이해하는 것 또한 중요하다는 것을 배웠습니다.
특히,
canvas 환경에서의 좌표계 문제나 상태 동기화 이슈를 직접 겪고 해결하는 것은 지금까지와 다른 구현과는 많이 다른 새로운 경험이였습니다.

 

728x90

'React' 카테고리의 다른 글

[React] LocalStorage(로컬스토리지) - 저장, 불러오기, 삭제  (1) 2025.05.29
[React] Props(프롭스)란?  (0) 2025.05.28
[React] 리액트 타입 연결하기  (0) 2025.05.28
[React] UseState란?  (0) 2025.05.27
[React] UseMemo란?  (0) 2025.05.22

 

프로젝트를 진행 중, API의 기능에 대한 질문을 했습니다. API의 역할 중 '상태 동기화'라는 새로운 패턴을 배우기까지의 과정을 기록해 보려고 합니다.


문제 : 몇 개의API를 만들어야 하는가?

관리자 페이지에서 '방' 목록을 자유롭게 편집(추가, 수정, 삭제)하고 '저장' 버튼을 누르면 그 결과를 서버에 반영하는 기능을 개발해야 합니다.


'새로 추가된 방은 POST로 보내고, 수정된 건 PATCH로, 삭제된 건 DELETE로 각각 API를 호출해야 한다' 라고 생각을 하였고, '클라이언트에서 방에 대하여 추가, 수정, 삭제하는로직이 필요하다' 라고 생각을 하였습니다.

 

그러나,  "여러 개의 로직을 만들 필요 없이, PUT API 하나로 클라이언트에서 최종 방 목록 전체를 PUT으로 보내면, 서버에서 기존 데이터와 비교해서 한 번에 처리하면 된다"라는 동료의 의견을 얻었습니다.

원인 분석

처음에는 동료의 의견에 대하여 이해하지 못했습니다.
'PUT은 상태 변경에 대하여 업데이트를 하는 api이고 어떤 방이 삭제되었는지에 대한 정보가 없으므로, 서버가 어떻게 삭제를 하는 것인가? DELETE API는 꼭 필요하지 않은가?' 라고 생각이 되었습니다.

 

이후, 동료에게 PUT 메소드에 대해 설명을 들었으며 그 내용은 "PUT은 단순히 데이터를 '수정'하는 것이 아니라, 리소스의 상태를 요청받은 상태로 '교체'하는 역할이라는 것" 입니다.

 

즉, 서버의 이전 상태가 어땠는지는 중요하지 않고 리소스의 최종 상태는 내가 보낸 모습과 똑같이 만든다는 것 입니다.

DELETE API가 필요 없는 이유는 '삭제'라는 행위 자체가 PUT의 '교체'라는 개념 안에 이미 포함되어 있기 때문입니다. 최종 목록에 없는 데이터는 자연스럽게 '제거되어야 할 대상'입니다.

 

이 설명을 듣고 다음과 같은 서버의 로직을 생각했습니다.

'상태 동기화' 패턴의 서버 로직

  1. 현실 파악 (SELECT): 먼저, DB에서 '기존 상태'의 방 목록을 전부 가져온다.
  2. 차이점 분석 (Compare): 클라이언트가 보낸 '최종 상태'와 '기존 상태'를 비교한다.
  3. 작업 수행 (Execute): 비교 결과를 바탕으로 필요한 C/U/D 작업을 한 번에 처리한다.
    Create: '최종 상태'에만 있는 방은 새로 추가한다.
    Update: 양쪽 모두에 있는 방은 정보를 갱신한다. (UPSERT)
    Delete: '기존 상태'에만 있던 방은 삭제한다.

해결

1. 클라이언트 : 최종 상태만 전송

클라이언트는 현재 화면에 보이는 rooms 배열을 PUT을 요청합니다. 복잡한 상태를 추적할 필요가 없습니다.

const spaceSave = async () => {
  try {
    await fetch(`/api/admin/spaces/${spaceId}`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        spaceName: adminName,
        // 현재 클라이언트의 rooms 상태 전체를 그대로 전달
        rooms: rooms.map(room => ({
          roomId: typeof room.id === 'number' ? room.id : undefined,
          name: room.name ?? '',
          // ... 나머지 데이터
        })),
      }),
    });
    alert('저장되었습니다.');
  } catch (error) {
    console.error('저장 실패:', error);
  }
};

2. 서버: 상태 동기화 로직 수행

서버의 PUT 핸들러가 '상태 동기화' 역할을 수행합니다.

export async function PUT(request: NextRequest, { params }) {
  try {
    const spaceId = Number(params.spaceId);
    const { spaceName, rooms: incomingRooms } = await request.json();

    // 1. DB에서 기존 방 목록 조회
    const existingRooms = await roomRepository.findBySpaceId(spaceId) ?? [];
    const existingRoomIds = new Set(existingRooms.map(r => r.id as number));

    // 2. 요청과 비교하여 C/U/D 목록 분류
    const incomingRoomIds = new Set(incomingRooms.filter(r => r.roomId).map(r => r.roomId));
    
    const roomsToCreate = incomingRooms.filter(r => !r.roomId);
    const roomsToUpdate = incomingRooms.filter(r => r.roomId);
    const roomIdsToDelete = [...existingRoomIds].filter(id => !incomingRoomIds.has(id));

    // 3. DB 작업 실행
    if (roomsToCreate.length > 0) { /* ... saveAll 로직 ... */ }
    if (roomsToUpdate.length > 0) { /* ... upsert 로직 ... */ }
    if (roomIdsToDelete.length > 0) { /* ... deleteByIds 로직 ... */ }

    return NextResponse.json({ message: '성공적으로 업데이트되었습니다.' });
  } catch (error) {
    return NextResponse.json({ error: '업데이트 실패' }, { status: 500 });
  }
}

정리

'상태 동기화' 패턴은 상황에 잘 맞추어서 사용해야 합니다.

장점 : 게시물의 태그 목록, 장바구니, 설문조사 항목처럼 하나의 리소스에 속한 여러 자식 요소들을 한 화면에서 자유롭게 편집할 때  권장됩니다. 클라이언트 로직이 단순해지며 단일 API 호출로 데이터 정합성을 지키기 쉬워집니다.

단점 : 데이터 양이 아주 많을 때는 전체 목록을 보내는 것이 비효율적입니다. 하나의 데이터만 바뀌어도 전체를 바꿔야 하는 문제가 생길 수 있으며, 이러한 경우에는 PATCH 같은 다른 방법을 고민해야 합니다.


간단한 CRUD 기능만 구현하는 작은 프로젝트이거나 개발 일정이 매우 촉박한 경우에는 사용을 추천합니다.

그러나,  '기존 데이터 조회 -> 비교 -> C/U/D 실행' 이라는 비교적 복잡한 로직 구현을 거쳐야 합니다. 이 구현 비용이 프로젝트의 규모나 일정에 비해 과도할 수 있습니다.

728x90

멋쟁이사자들 프론트엔드 심화 부트캠프에서 개인적인 목표로 하루에 한 개씩 공부한 내용을 포스트 하는 것을 개인 목표로 삼았으나, 프로젝트에 집중하여 목표를 놓쳤습니다. 다시 하루에 한 개씩 배운 내용을 포스트 하겠습니다.


프로젝트 '따북'을 하며 느낀 점에 대하여 KPT 방식의 개인 회고를 작성하겠습니다.

KEEP (앞으로도 계속 이어갈 좋은 습관 및 활동)

API 명세에 대한 깊이 있는 토론 : PUT DELETE의 역할을 두고 동료와 나눈 토론이 매우 긍정적이었습니다. 이는 단순히 기능을 구현하는 것이 아닌, "왜 이 기술을 이렇게 사용해야 하는가"에 대한 근본적인 이해를 높이는 데 큰 도움이 되었습니다. 이러한 기술 토론은 계속 이어 나가겠습니다.

클린 아키텍처 원칙에 대한 고민 : 프로젝트 진행 중, "이것이 클린 아키텍처에 맞는가?" 질문에 대하여 계속 생각했습니다. 코드의 구조와 유지보수성을 생각하는 습관에 대하여 배웠습니다.
포기하지 않는 점진적인 디버깅 : 처음에는 동기화 문제, 그 다음엔 API 설계 문제, 마지막에는 타입 에러 문제까지 단계별로 문제를 해결했습니다. 복잡한 문제를 마주친 경우, 한 번에 해결하는 것이 아닌 천천히 하나씩 원인을 분석하고 해결하는 접근 방식을 배웠습니다.

명확한 요구사항 전달 및 피드백 : "POST를 사용하면 안 된다" 와 같이 막히는 부분에서 명확하게 지향하는 바와 제약사항에 대하여 인지하고 작업을 이어나갔습니다. 이는 불필요한 커뮤니케이션 줄이고 문제 해결의 방향을 명확하게 설정하는 데 큰 도움이 되었습니다.

Problem (개선이 필요한 문제점 및 어려움)

API 설계와 실제 구현 간의 초기 괴리 : PUT은 리소스 전체를 교체한다는 '설계 원칙'과, '현실적인 구현'의 차이를 초기에 명확히 인지하지 못했습니다. 이로 인해 클라이언트와 서버 간 데이터 동기화 로직이 누락되었습니다.

암묵적인 가정에 따른 불필요한 작업 : 처음에는 "새로운 공간을 생성할 수도 있다"는 암묵적인 가정 하에 POST 로직을 포함하여 코드를 작성하였으나, "공간은 이미 존재한다"는 핵심 제약 조건이 나중에 명확해져 로직을 수정해야 했습니다.

계층 간 데이터 타입(DTO)의 불일치: UpdateRequest 타입에 spaceId가 누락되어 발생한 타입 에러는, 각 계층(Controller-Usecase-Repository)을 데이터가 이동할 때 타입 정의가 일관되지 않으면 어떤 문제가 발생하는지 명확히 보여주었습니다.

상태 동기화 로직의 복잡성: 클라이언트의 최종 상태(rooms 배열)를 기준으로 서버 DB의 데이터를 C/U/D 처리하는 '상태 동기화' 로직이 직관적이지 않으며, 구현 시 고려해야 할 상황이 많아 처음 접근하기에 다소 복잡하게 느껴졌습니다.

TRY (문제점을 해결하기 위해 다음에 시도해 볼 것)

API 설계 시, '서버 구현 시나리오' 함께 정의하기 : 앞으로 API 엔드포인트를 설계할 때, 명세(PUT /api/spaces/{id}) 뿐만 아니라, 해당 요청을 받았을 때 서버가 수행해야 할 작업의 순서(e.g., 1. 기존 데이터 조회 -> 2. 비교 -> 3. 생성/수정/삭제 실행)를 함께 구상하고 문서화하는 습관을 들이겠습니다.

'타입 우선 설계(Type-First Design)' 접근법 시도 : 기능을 구현하기 전에, 해당 기능에 필요한 데이터의 흐름을 먼저 정의하고 그에 맞는 타입(Type/Interface)도 함께 생각하겠습니다. 특히 서버와 클라이언트가 주고받는 데이터 객체(DTO)의 타입을 가장 먼저 확정하여, 계층 간 타입 불일치 문제를 원천적으로 방지하겠습니다.

핵심 제약 조건 및 유저 스토리 명시적으로 공유 : 새로운 기능 개발 착수 전, "사용자는 ~를 할 수 있다/없다", "이 데이터는 항상 존재한다/할 수 있다" 와 같은 핵심적인 제약 조건을 먼저 확인하고 팀원과 공유하는 절차를 갖겠습니다.


복잡한 로직에 대한 패턴 학습 및 적용 :
 이번에 배운 '상태 동기화(State Synchronization)' 패턴을 잘 기억하고 문서화 하겠습니다.
다음에 유사한 "목록 전체를 편집하는 UI"를 구현할 때, 개발 시간을 단축하고 버그를 줄이겠습니다.

728x90

 

Module not found: Can't resolve 'canvas' 에러 해결

 

사용자가 직접 아이템을 배치하고 크기를 조절할 수 있는 동적인 캔버스 에디터 기능을 추가하고 싶었습니다.
Konva.js를 사용을 결정했고 인스톨 후에, yarn run dev를 실행하자 Module not found: Can't resolve 'canvas' 에러가 발생했습니다. 에러 해결 과정에서 배우게 된 Next.js의 핵심 렌더링 개념에 대해 이야기합니다.


"'canvas' 모듈을 찾을 수 없다."
Konva 라이브러리 내부 코드(
konva/lib/index-node.js)가 require('canvas')를 호출하지만, 제 프로젝트는 해당 패키지가 존재하지 않았습니다.

 

Canvas 패키지 설치

// npm 사용자
npm install canvas

// yarn 사용자
yarn add canvas

위와같은 명령어를 터미널에 입력하면 패키지가 저장되어 konva의 사용이 가능해집니다.

SSR 렌더링 또는 CSR 렌더링

Next.js의 렌더링 환경 또한 영양을 미칠 수 있습니다.. Next.js 13 버전부터 도입된 App Router는 기본적으로 모든 컴포넌트를 서버 컴포넌트(Server Components)로 취급합니다. 즉, 서버에서 렌더링을 시도합니다.

하지만 Konva.js는 브라우저의 <canvas> 엘리먼트를 직접 조작하고, 사용자의 마우스 클릭, 드래그 같은 이벤트를 처리해야 합니다.

이는 windowdocument 객체가 존재하는 클라이언트 환경에서만 정상적으로 동작함을 의미합니다.

서버에는 브라우저, 화면, document등이 없습니다. 그러므로 서버 환경에서 Konva.js를 실행한다면 에러가 발생합니다.

해결책: 'use client'

Next.js는 이런 상황을 위한 해결책이 존재하며, 'use client' 지시어로 해결 가능합니다.

파일 최상단에 'use client';를 추가하면, Next.js는 컴포넌트가 import하는 모든 자식들은 서버가 아니라 클라이언트에서 렌더링합니다.

 

// components/CanvasEditor.tsx

'use client'; // 이 컴포넌트는 클라이언트에서만 렌더링됩니다.

import React, { useEffect, useRef } from 'react';
import Konva from 'konva';

const CanvasEditor = () => {
  const containerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (containerRef.current) {
      const stage = new Konva.Stage({
        container: containerRef.current,
        width: 500,
        height: 500,
      });

      const layer = new Konva.Layer();
      stage.add(layer);

      const rect = new Konva.Rect({
        x: 50,
        y: 50,
        width: 100,
        height: 100,
        fill: 'skyblue',
        draggable: true,
      });
      layer.add(rect);
    }
  }, []);

  return <div ref={containerRef} />;
};

export default CanvasEditor;

동적 임포트(Dynamic Import)

한 걸음 더 나아가 최적화할 수 있는 방법도 있으며, next/dynamic의 사용입니다. Konva.js는 무거운 라이브러리 이므로 첫 페이지 로드에 포함시킬 필요가 없을 수 있습니다.

dynamic import를 사용하면 해당 컴포넌트를 서버 사이드 렌더링에서 완전히 제외하고, 필요할 때 클라이언트에서만 불러올 수 있습니다.

      import dynamic from 'next/dynamic';

const CanvasEditor = dynamic(
  () => import('@/components/CanvasEditor'),
  { 
    ssr: false, // 서버 사이드 렌더링을 명시적으로 비활성화
    loading: () => <p>Loading canvas...</p> // 로딩 중 보여줄 UI
  }
);

// 페이지에서 이 CanvasEditor를 사용합니다
export default function MyPage() {
  return (
    <div>
      <h1>My Awesome Editor</h1>
      <CanvasEditor />
    </div>
  );
}

위의 방법 사용시 초기 로딩 성능을 개선하는 효과도 얻을 수 있습니다.

정리

버그 픽스 뿐만이 아닌 Next.js의 서버 컴포넌트와 클라이언트 컴포넌트의 차이를 알게 되었습니다.

  • 문제: Konva.js 사용 시 Module not found: 'canvas' 에러 발생
  • 원인: window, document 객체가 필요한 클라이언트 전용 라이브러리를 서버 환경(서버 컴포넌트)에서 렌더링하려고 시도함.
  • 해결: 컴포넌트 파일 최상단에 'use client';를 추가하여 클라이언트 컴포넌트로 전환.
  • 심화: next/dynamicssr: false 옵션을 사용하여 코드 스플리팅 및 성능 최적화.
728x90

'Next.js' 카테고리의 다른 글

Next.js란?  (0) 2025.07.05