'어떻게 하면 좋은 습관을 꾸준히 이어갈 수 있을까?'라는 아이디어로 '해빗(Habit)' 프로젝트를 진행하게 되었습니다.

'해빗' 서비스 소개

'해빗'은 과학적인 습관 형성 원리에 기반하여 사용자의 꾸준한 성장을 돕는 서비스입니다. 저희는 **'21일'**과 **'66일'**이라는 습관 형성의 중요한 이정표를 중심으로, 사용자가 지치지 않고 목표를 달성하는 경험을 제공하고자 합니다.

핵심 기능

  • 체계적인 챌린지 : 모든 습관은 21일 챌린지로 시작하며, 성공 시 66일 챌린지로 이어져 습관의 완전한 내재화를 돕습니다.
  • 성장 기록 : 전용 캘린더와 마이페이지를 통해 자신의 습관 여정과 성과를 한눈에 확인할 수 있습니다.
  • AI 코치의 피드백 : 달성률에 따라 AI가 격려와 피드백을 전달하여 꾸준한 동기를 부여합니다.
  • 소셜 기능 : 팔로우, 팔로워 기능으로 다른 사용자들과 함께 응원하며 성장할 수 있습니다.

각 기능에 대한 세부 명세를 정의하고 백엔드 설계를 마쳤습니다. 탄탄한 설계로 안정적이고 사용할 수 있는 서비스를 만들어가겠습니다.

728x90

쿼리스트링, 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