넥스트 15 - 한페이지로 알아보기
소스 : https://github.com/braverokmc79/GeniusGPT-nextjs-tutorial
1. Create New Next.js App
Next.js 프로젝트 생성 명령어 실행:
npx create-next-app@latest 프로젝트이름
예를 들어, 프로젝트 이름을 nextjs-tutorial로 설정하려면
npx create-next-app@latest nextjs-tutorial
1.TypeScript 사용 여부 (Yes/No):
- 예(Yes): TypeScript를 사용하여 프로젝트를 생성합니다. TypeScript는 정적 타입 체크를 제공하여 코드의 안정성을 높이고, 에디터에서 자동 완성 및 오류 검출 기능을 강화합니다.
- 아니요(No): JavaScript만 사용하여 프로젝트를 생성합니다. 이 경우 타입 체크 기능은 제공되지 않습니다.
2.ESLint 사용 여부 (Yes/No):
- 예(Yes): ESLint를 설정하여 코드 품질을 관리합니다. ESLint는 JavaScript 및 TypeScript 코드에서 코드 스타일을 검사하고, 잠재적인 오류나 코드 일관성 문제를 찾아줍니다.
- 아니요(No): ESLint를 설정하지 않으며, 코드 스타일을 수동으로 관리합니다.
3.Tailwind 사용 여부 (Yes/No):
- 예(Yes): Tailwind CSS를 사용하여 프로젝트를 스타일링합니다. Tailwind는 유틸리티 클래스 기반의 CSS 프레임워크로, 클래스명을 통해 빠르게 스타일을 적용할 수 있습니다.
- 아니요(No): Tailwind CSS를 사용하지 않고 다른 방식으로 스타일을 작성합니다.
4.Source 디렉토리 사용 여부 (Yes/No):
- 예(Yes): 프로젝트 구조에서 src 디렉토리를 사용합니다. src 디렉토리 내에 애플리케이션의 주요 소스 코드를 포함시켜 구조를 명확하게 유지할 수 있습니다.
- 아니요(No): src 디렉토리 없이 프로젝트 구조를 구성합니다.
Next.js 프로젝트 설정 시 src/ 디렉토리 사용 여부를 묻는 질문에서 기본값이 No로 설정된 이유는 대부분의 Next.js 프로젝트에서 src/ 디렉토리를 꼭 필요로 하지 않는 경우가 많기 때문입니다.
src/ 디렉토리란?
- src/ 디렉토리는 프로젝트의 소스 코드를 체계적으로 관리하기 위해 사용하는 선택적인 디렉토리입니다.
- Next.js는 src/ 디렉토리를 사용하지 않아도 기본적으로 모든 기능이 정상적으로 작동합니다.
보통 No로 사용하는 이유
간단한 구조 유지
- 작은 프로젝트나 디렉토리 계층이 복잡하지 않은 경우, src/ 디렉토리를 사용하지 않고도 충분히 관리가 가능합니다.
기본값에 따르는 관습
- Next.js의 초기 설정에서 src/를 사용하지 않는 것이 기본값이므로, 많은 사용자들이 이를 그대로 따릅니다.
추가 설정 필요 없음
- src/ 디렉토리를 도입하면 디렉토리 구조를 재구성해야 하고, 일부 사용자는 이를 번거롭게 느낄 수 있습니다.
5.App Router 사용 여부 (Yes/No):
- 예(Yes): App Router를 사용하여 페이지와 API를 관리합니다. App Router는 Next.js 13에서 도입된 기능으로, 라우팅을 관리하는 새로운 방법입니다. app 디렉토리를 사용해 페이지 및 서버 코드를 쉽게 구조화할 수 있습니다.
- 아니요(No): 전통적인 pages 디렉토리 기반 라우팅을 사용합니다.
6. Turbopack이란?
- Turbopack은 Vercel에서 Next.js를 위해 개발한 초고속 번들러입니다.
- 기존 Webpack을 대체할 목적으로 개발되었으며, 특히 개발 환경에서 더 빠른 빌드 속도와 핫 모듈 리플레이스먼트(HMR) 성능을 제공합니다.
- 현재는 실험적(experimental) 단계에 있으며, Next.js 최신 버전에서 옵션으로 제공됩니다.
Turbopack을 사용해야 할까?
프로젝트 초기나 실험 단계라면:
- Turbopack을 시도해보는 것도 좋습니다. 빌드 속도가 개선될 가능성이 높습니다.
안정성을 중시하거나, 프로덕션(배포 준비된) 프로젝트라면:
- Turbopack은 아직 실험 단계이기 때문에, 기본 Webpack을 사용하는 것이 더 안전할 수 있습니다.
사용 방법
Turbopack 사용:
- next dev 실행 시 "Yes"를 선택하거나, 터미널에서 NEXT_SKIP_TURBOPACK=0 next dev 명령어를 실행합니다.
기본 Webpack 사용:
- "No"를 선택하거나, next dev를 그대로 실행하면 Webpack을 사용합니다.
6.Alias 사용자 정의 여부 (Yes/No):
- 예(Yes): 모듈 경로에 별칭(alias)을 설정하여 코드에서 경로를 더 간단하고 깔끔하게 작성할 수 있습니다. 예를 들어, @components와 같은 별칭을 설정하여 파일 경로를 쉽게 참조할 수 있습니다.
- 아니요(No): 별칭을 설정하지 않고 상대 경로로 파일을 참조합니다.
이 프로젝트에서는 전부 YES 한다.
1.TypeScript 사용 여부: 아니요(Yes)
2.ESLint 사용 여부: 아니요(Yes)
3.Tailwind 사용 여부: 예(Yes)
4.Source 디렉토리 사용 여부: 아니요(Yes)
5.App Router 사용 여부: 예(Yes)
6.Turbopack을 사용하시겠습니까?
7.Alias 사용자 정의 여부: 아니요(Yes)
$ npx create-next-app@latest nextjs-tutorial √ Would you like to use TypeScript? ... No / Yes √ Would you like to use ESLint? ... No / Yes √ Would you like to use Tailwind CSS? ... No / Yes √ Would you like your code inside a `src/` directory? ... No / Yes √ Would you like to use App Router? (recommended) ... No / Yes √ Would you like to use Turbopack for `next dev`? ... No / Yes ? Would you like to customize the import alias (`@/*` by default)? » No / Yes What import alias would you like configured? ... @/*
2.파일 및 폴더 구조
app 폴더
- 작업의 대부분을 진행하는 폴더로, 여기에서 라우트, 레이아웃, 로딩 상태, 에러 상태 등을 설정함.
node_modules 폴더
- 프로젝트 의존성이 저장되는 폴더.
public 폴더
- 정적 파일(이미지, 아이콘 등)을 저장하는 폴더.
.gitignore 파일
- 소스 제어에서 무시할 파일들을 설정.
- 나중에 .env 파일 추가 시에도 사용.
설정 파일
- 프로젝트 설정과 관련된 여러 구성 파일들로, 진행하면서 설명 예정.
package.json 파일
- 유용한 스크립트, 프로젝트 의존성 및 개발 의존성을 확인할 수 있음.
- Tailwind CSS를 사용하기 때문에 Autoprefixer 및 CSS 관련 라이브러리가 포함됨.
주요 스크립트
npm run dev
- 프로젝트를 로컬에서 실행하는 데 주로 사용하는 스크립트.
- 실행 시 localhost:3000에서 프로젝트가 구동됨.
npm run build 및 npm start
- build: 프로젝트를 로컬 컴퓨터에 프로덕션 빌드로 설정.
- start: 프로덕션 빌드를 실행.
실행 절차
- 통합 터미널을 열고 npm run dev 명령어 실행.
- 모든 설정이 올바르다면 **localhost:3000**에서 프로젝트 홈 페이지를 확인 가능.
$ npm run dev > nextjs-tutorial@0.1.0 dev > next dev --turbopack ▲ Next.js 15.1.4 (Turbopack) - Local: http://localhost:3000 - Network: http://100.126.31.201:3000 ✓ Starting... ✓ Ready in 5.3s
3.Home Page
page.tsx
import React from 'react' const HomePage:React.FC = () => { return ( <div className='text-7xl'> <h1>Home</h1> </div> ) } export default HomePage;
4.More Page
새로운 페이지 생성 방법
폴더 생성
- app 폴더 안에서 새로운 폴더를 생성.
- 폴더 이름이 URL의 세그먼트가 됨.
예: about 폴더를 생성하면 URL은 localhost:3000/about 로 접근 가능
page 파일 생성
- 폴더 내부에 반드시 page 라는 이름의 파일을 생성해야 함.
- 파일 확장자는 .js, .jsx, .tsx 중 선택 가능.
- 중요: 파일 이름은 반드시 page여야 하며, 다른 이름(예: banana)으로 설정 시 동작하지 않음.
예제: about 페이지 생성
about 폴더 생성
- app 폴더 안에 about 폴더를 생성.
page.js 파일 생성
- about 폴더 안에 page.js 파일 생성.
- homepage와 유사하게 React 컴포넌트를 작성.
- 간단히 복사-붙여넣기 후 컴포넌트 이름을 **About**으로 변경.
브라우저에서 확인
- 브라우저에서 localhost:3000/about로 이동.
- 새로 생성된 about 페이지가 정상적으로 표시되는지 확인.
5.Link Component
페이지 간 네비게이션 구현
Link 컴포넌트 사용
- **next/link**에서 Link 컴포넌트를 가져옴.
- href 속성을 통해 이동할 경로를 지정.
예제: 홈 → About, About → 홈
- 홈 페이지에서 About 페이지로 이동: href="/about"
- About 페이지에서 홈 페이지로 이동: href="/"
import Link from 'next/link'; import React from 'react' const AboutPage:React.FC = () => { return ( <div> <h1 className='text-7xl'>AboutPage</h1> <Link href='/' className='text-2xl'> home page </Link> </div> ) } export default AboutPage;
6. Nested Routes
Nested Routes는 Next.js에서 페이지 라우팅을 구성할 때 사용되는 개념으로,
중첩된 URL 구조를 통해 계층적(부모-자식 관계) 페이지를 관리하는 방식입니다. Next.js에서는 기본적으로 파일 기반 라우팅을 사용하기 때문에 폴더 구조를 활용해 쉽게 Nested Routes를 구현할 수 있습니다.
Nested Routes의 기본 개념
- 폴더 구조에 따라 URL 경로가 결정됩니다.
- 부모 경로와 자식 경로를 계층적으로 나눠서 구성할 수 있습니다.
- Nested Routes는 중첩된 UI를 구성하거나 동적 경로를 사용할 때 유용합니다.
Nested Routes 구현하기
1. 폴더 및 파일 구조
다음과 같은 폴더 구조를 가진다고 가정합니다.
/app ├── layout.tsx # 공통 레이아웃 ├── page.tsx # 기본 페이지 ("/") ├── blog/ │ ├── layout.tsx # 블로그 레이아웃 ("/blog") │ ├── page.tsx # 블로그 메인 페이지 ("/blog") │ ├── [id]/ │ ├── page.tsx # 특정 블로그 글 ("/blog/:id")
src/app/about/[id]/page.tsx
구조
/app ├── about/ │ ├── [id]/ │ ├── page.tsx
import Link from 'next/link'; import React from 'react'; interface AboutIdPageProps { params: Promise<{ id: string }>; } const AboutIdPage:React.FC<AboutIdPageProps> = async ({ params }) => { const { id } = await params return ( <div> <h1 className="text-7xl mb-10">About Id Page -- {id}</h1> <Link href="/" className="text-2xl text-blue-400"> Home Page </Link> </div> ); }; export default AboutIdPage;
Dynamic APIs are Asynchronous 참조
https://nextjs.org/docs/messages/sync-dynamic-apis
7. 페이지 만들기
app/ ├── client/ │ └── page.tsx ├── drinks/ │ └── page.tsx ├── prisma-example/ │ └── page.tsx ├── query/ │ └── page.tsx └── tasks/ └── page.tsx
위 디렉토리 구조에서 각 폴더는 해당 경로(URL)를 나타내며, page.tsx 파일은 각 페이지의 내용을 담당합니다.
8. CSS
CSS 설정 및 Daisy UI 설치 과정
1. 글로벌 스타일(Global CSS)
- global.css 파일에서 애플리케이션 전역 스타일을 설정할 수 있습니다.
- 현재는 Tailwind CSS 지시어(Directives)가 사용되고 있지만, Vanilla CSS를 사용하는 경우 CSS 클래스를 작성하고 JSX에서 참조하면 됩니다.
2. Daisy UI 소개
- Daisy UI는 Tailwind 기반의 컴포넌트 라이브러리로, 프로젝트를 더 빠르고 간편하게 개발할 수 있도록 돕습니다.
- Daisy UI를 사용하면 Tailwind 클래스를 적게 사용하면서도 깔끔한 결과물을 얻을 수 있습니다.
3. Daisy UI와 Tailwind Typography 설치
- Daisy UI 설치 명령어:
npm install daisyui --save-dev
Tailwind Typography 플러그인 설치 명령어
npm install @tailwindcss/typography --save-dev npm install -D @tailwindcss/typography
- Daisy UI는 Tailwind Typography 플러그인을 필요로 하므로 함께 설치해야 합니다.
4. Tailwind Config 설정
- tailwind.config.js 파일의 plugins 배열에 플러그인을 추가합니다.
- Daisy UI 문서에 따르면, Typography 플러그인을 먼저 추가하고, 그다음 Daisy UI를 추가하는 것이 좋습니다.
- 설정 예시:
import type { Config } from "tailwindcss"; import typography from '@tailwindcss/typography'; import daisyui from 'daisyui'; export default { content: [ "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", "./src/components/**/*.{js,ts,jsx,tsx,mdx}", "./src/app/**/*.{js,ts,jsx,tsx,mdx}", ], theme: { extend: { colors: { background: "var(--background)", foreground: "var(--foreground)", }, }, }, plugins: [ typography, daisyui, ], } satisfies Config;
5. 개발 서버 실행
- 설정 후, 개발 서버를 다시 실행합니다:
npm run dev
9. Tailwind CSS
Tailwind CSS 주요 개념 요약
1. Tailwind CSS 개요
- Tailwind CSS는 유틸리티 클래스 기반의 CSS 프레임워크로, CSS를 별도로 작성하지 않고 HTML(또는 JSX)에서 바로 스타일을 적용할 수 있습니다.
- 프로젝트 진행 시 주로 사용하는 클래스는 제한적이며, 자주 사용하는 클래스는 자연스럽게 기억됩니다.
- 필요할 경우 Tailwind 문서를 참고하여 원하는 스타일이나 설정을 쉽게 검색할 수 있습니다.
2. Tailwind CSS의 기본 아이디어
- 기존 방식:
- HTML 요소에 커스텀 클래스를 추가하고, CSS 파일에서 스타일을 정의.
- Tailwind 방식:
- 유틸리티 클래스를 HTML 또는 JSX에 직접 추가하여 스타일을 정의.
- 예:
- p-6 → 패딩 1.5rem(24px).
- bg-sky-500 → 배경색 하늘색(500 단계).
3. 주요 Tailwind CSS 개념
(1) 색상 시스템
- Tailwind는 미리 정의된 색상 팔레트와 다양한 **색상 단계(50~950)**를 제공합니다.
- 예: bg-blue-500 → 파란색의 500단계.
- 프로젝트 전반의 일관된 색상 디자인을 손쉽게 구현 가능.
- 필요 시 색상 커스터마이징 가능.
(2) 간격(Spacing)
- 패딩(Padding)과 마진(Margin):
- p-1 → 0.25rem(4px) 패딩.
- p-4 → 1rem(16px) 패딩.
- 특정 방향만 설정:
- pt-2 → 상단 패딩 0.5rem.
- px-4 → 좌우 패딩 1rem.
- 폭(Width):
- w-4 → 폭 1rem(16px).
- w-96 → 폭 24rem(384px).
- w-full → 부모 요소의 전체 너비에 맞춤.
(3) 반응형 디자인
- 미디어 쿼리를 별도로 작성하지 않고, 클래스에 반응형 키워드 추가:
- w-16 → 기본 작은 화면에서 폭 4rem.
- md:w-32 → 중간 화면(768px 이상)에서 폭 8rem.
- 반응형 키워드: sm, md, lg, xl, 2xl.
(4) Flexbox 및 Grid
- Flexbox:
- flex → 플렉스박스 부모 설정.
- 기본은 행(row), flex-col을 추가하면 열(column)로 변경.
- 기타 클래스: flex-wrap, justify-center, items-start 등.
- Grid:
- grid → 그리드 부모 설정.
- 예: grid-cols-3 → 3열 그리드.
(5) 호버 및 상태 클래스
- 상태 클래스: hover, focus 등.
- 예: hover:bg-sky-700 → 호버 시 배경색 변경.
10. DaisyUI 라이브러리 소개 및 사용법
https://daisyui.com/components/dropdown/
1. DaisyUI란?
DaisyUI는 Tailwind CSS 기반으로 만들어진 컴포넌트 라이브러리입니다.
이를 통해 버튼, 모달, 드롭다운 같은 UI 요소를 처음부터 구현할 필요 없이 제공된 클래스를 활용해 빠르게 개발할 수 있습니다.
2. DaisyUI의 특징
- Tailwind CSS와 동일한 방식으로 동작하며, 추가 클래스로 컴포넌트를 간단히 구현.
- Tailwind의 유틸리티 클래스와 함께 사용 가능.
- 커스터마이징 가능: 제공된 클래스 위에 Tailwind 클래스를 추가하여 디자인 변경 가능.
- 빠른 작업 속도: 클래스 몇 개만으로 미려한 UI 구성.
3. 주요 사용법
버튼 생성
DaisyUI를 설치 후, 버튼 생성은 매우 간단합니다.
<button className="btn">Button</button> <button className="btn btn-neutral">Neutral</button> <button className="btn btn-primary">Primary</button> <button className="btn btn-secondary">Secondary</button> <button className="btn btn-accent">Accent</button> <button className="btn btn-ghost">Ghost</button> <button className="btn btn-link">Link</button>
- btn: 기본 버튼 클래스.
- btn-primary: 주 색상을 가진 버튼.
다양한 크기와 스타일 제공:
- 버튼 크기: btn-lg, btn-sm, btn-xs 등.
- 원형 버튼: btn-circle.
- 로딩 버튼: btn-loading.
2. 드롭다운
<details className="dropdown"> <summary className="btn m-1">open or close</summary> <ul className="menu dropdown-content bg-base-100 rounded-box z-[1] w-52 p-2 shadow"> <li><a>Item 1</a></li> <li><a>Item 2</a></li> </ul> </details>
3.모달
{/* Open the modal using document.getElementById('ID').showModal() method */} <button className="btn" onClick={()=>document.getElementById('my_modal_1').showModal()}>open modal</button> <dialog id="my_modal_1" className="modal"> <div className="modal-box"> <h3 className="font-bold text-lg">Hello!</h3> <p className="py-4">Press ESC key or click the button below to close</p> <div className="modal-action"> <form method="dialog"> {/* if there is a button in form, it will close the modal */} <button className="btn">Close</button> </form> </div> </div> </dialog>
- modal: 모달 컨테이너.
- modal-box: 모달 내용.
4.캐러셀
<div className="carousel carousel-vertical rounded-box h-96"> <div className="carousel-item h-full"> <img src="https://img.daisyui.com/images/stock/photo-1559703248-dcaaec9fab78.webp" /> </div> <div className="carousel-item h-full"> <img src="https://img.daisyui.com/images/stock/photo-1565098772267-60af42b81ef2.webp" /> </div> <div className="carousel-item h-full"> <img src="https://img.daisyui.com/images/stock/photo-1572635148818-ef6fd45eb394.webp" /> </div> <div className="carousel-item h-full"> <img src="https://img.daisyui.com/images/stock/photo-1494253109108-2e30c049369b.webp" /> </div> <div className="carousel-item h-full"> <img src="https://img.daisyui.com/images/stock/photo-1550258987-190a2d41a8ba.webp" /> </div> <div className="carousel-item h-full"> <img src="https://img.daisyui.com/images/stock/photo-1559181567-c3190ca9959b.webp" /> </div> <div className="carousel-item h-full"> <img src="https://img.daisyui.com/images/stock/photo-1601004890684-d8cbf643f5f2.webp" /> </div> </div>
- carousel: 캐러셀 컨테이너.
- carousel-item: 각각의 캐러셀 아이템.
4. 테마 설정 및 활용
DaisyUI는 다양한 테마를 제공하며, Tailwind와 다른 방식으로 색상을 설정합니다.
기본 색상 사용
<div class="bg-primary text-primary-content">내용</div>
- bg-primary: 기본 배경색.
- text-primary-content: 기본 텍스트 색상.
2.테마 적용
- DaisyUI는 HTML의 data-theme 속성을 활용하여 테마를 지정합니다.
- 테마 변경은 매우 간단하며, 기본적으로 제공된 여러 테마 중 선택 가능합니다.
<html data-theme="dark"> <!-- 다크 테마 -->
커스텀 테마
- DaisyUI는 필요 시 맞춤형 테마를 생성할 수도 있습니다.
5. Typography 플러그인
DaisyUI는 Tailwind의 타이포그래피 플러그인을 설치해 사용할 것을 권장합니다.
// tailwind.config.js 설정 예시 plugins: [require('@tailwindcss/typography')]
정리
DaisyUI는 Tailwind CSS의 강력함과 함께 빠른 UI 개발을 위한 간편한 솔루션을 제공합니다.
테마 설정, 컴포넌트 활용, 커스터마이징 등 유연한 개발 환경을 통해 생산성을 높일 수 있습니다.
DaisyUI와 shadcn/ui
1. DaisyUI
특징:
- Tailwind CSS 위에서 작동하는 컴포넌트 기반 UI 라이브러리.
- 테마 시스템이 내장되어 있어 다크 모드나 맞춤 테마 설정이 간단함.
- 버튼, 모달, 캐러셀 등 미리 디자인된 컴포넌트를 제공.
- HTML 구조가 직관적이며, 적은 설정으로 빠르게 사용할 수 있음.
사용 사례:
- 스타일링과 UI 컴포넌트를 간단히 구현하려는 경우.
- 작은 규모의 프로젝트 또는 MVP 제작.
- 테마 변경이 잦은 프로젝트.
사용 현황:
- React 및 Next.js에서 사용하기 적합하지만, 대규모 애플리케이션에서는 유연성이 떨어질 수 있음.
- 빠르게 결과물을 내야 하는 프로젝트에서 주로 채택됨.
2. shadcn/ui
특징:
- Radix UI를 기반으로 **접근성(Accessibility)**이 뛰어난 Tailwind 스타일링 컴포넌트를 제공.
- DaisyUI와는 달리 컴포넌트를 직접 복사하여 사용하는 방식으로 설계.
- **React 서버 컴포넌트(RSC)**와 호환되며, TypeScript 지원이 강력함.
- 개발자에게 구조와 디자인의 완전한 제어권을 부여.
- **정적 사이트 생성(SSG)**과 같은 Next.js 기능과 잘 어울림.
사용 사례:
- 대규모 프로젝트, 특히 디자인 시스템을 구축해야 하는 경우.
- 접근성과 커스터마이징이 중요한 프로젝트.
- 더 많은 제어권을 요구하는 Next.js 기반 프로젝트.
사용 현황:
- Next.js와 같은 최신 React 프레임워크에서 점점 인기를 얻는 중.
- 개발자들이 컴포넌트를 직접 관리할 수 있는 점에서 선호됨.
4. 어떤 라이브러리를 선택할까?
DaisyUI가 적합한 경우:
- UI를 빠르게 구축해야 하는 MVP 프로젝트.
- 복잡한 커스터마이징보다는 즉시 사용 가능한 컴포넌트가 필요한 경우.
- 테마 변경과 Tailwind CSS의 기본 스타일을 선호하는 경우.
shadcn/ui가 적합한 경우:
- 접근성과 유연성이 중요한 대규모 프로젝트.
- Radix UI 기반의 강력한 기능과 Tailwind CSS의 자유로운 스타일링을 활용하고자 하는 경우.
- Next.js와 최신 React 기술을 적극적으로 활용하고 싶은 경우.
5. 결론
현재 커뮤니티에서는 shadcn/ui가 Next.js와 같은 최신 기술 스택에서 점점 더 많이 사용되고 있습니다.
특히, Radix UI 기반의 접근성, TypeScript 호환성, 유연한 커스터마이징이 주요 이유입니다. 반면, DaisyUI는 간단하고 직관적인 작업이 필요한 프로젝트에서 여전히 강력한 선택지입니다.
프로젝트 요구사항과 팀의 스타일에 맞춰 선택하는 것이 중요합니다.
11. Layout File
layout.tsx
import type { Metadata } from "next"; import { Geist, Noto_Sans_KR } from "next/font/google"; import "./globals.css"; const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"], }); const notoSansKR = Noto_Sans_KR({ subsets: ["latin"], weight: ["400", "700"], // 필요한 가중치 추가 }); export const metadata: Metadata = { title: "Next.js Tutorial", description: "넥스트 연습", keywords: ["Next.js", "React", "웹 개발", "튜토리얼"], }; export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return ( <html lang="ko"> <body className={`${geistSans.variable} ${notoSansKR.className} antialiased`}> <nav className="text-2xl text-primary">this is navbar</nav> {children} </body> </html> ); }
12. Navbar 만들기
src/app/components/NavBar.tsx
import Link from 'next/link' import React from 'react' const links = [ {href :'/client', label:'Client Page'}, {href :'/drinks', label:'Drinks Page'}, {href :'/tasks', label:'Tasks Page'}, {href :'/query', label:'Query Page'}, ] const NavBar = () => { return ( <nav className='bg-base-300 py-4'> <div className='navbar px-8 max-w-6xl mx-auto flex-col sm:flex-row'> <Link href='/' className='btn btn-primary'> 넥스트 </Link> <ul className='menu menu-horizontal md:ml-8'> {links.map((link)=>{ return ( <li key={link.href}> <Link href={link.href}>{link.label}</Link> </li> ) })} </ul> </div> </nav> ) } export default NavBar
/src/app/layout.tsx
import type { Metadata } from "next"; import { Geist, Noto_Sans_KR } from "next/font/google"; import "./globals.css"; import NavBar from "@/components/NavBar"; const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"], }); const notoSansKR = Noto_Sans_KR({ subsets: ["latin"], weight: ["400", "700"], // 필요한 가중치 추가 }); export const metadata: Metadata = { title: "Next.js Tutorial", description: "넥스트 연습", keywords: ["Next.js", "React", "웹 개발", "튜토리얼"], }; export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return ( <html lang="ko"> <body className={`${geistSans.variable} ${notoSansKR.className} antialiased`}> <NavBar /> <main className="px-8 py-20 max-w-6xl mx-auto"> {children} </main> </body> </html> ); }
13.Next.js에서 사용할 수 있는 컴포넌트 유형
Next.js에서는 컴포넌트를 크게 두 가지로 나눌 수 있습니다.
1. 서버 컴포넌트 (Server Components)
- 이름 그대로, 서버에서 렌더링되는 컴포넌트입니다.
- Next.js의 기본 설정은 모든 컴포넌트를 서버 컴포넌트로 처리합니다.
즉, 우리가 이미 생성한 페이지나 컴포넌트는 기본적으로 서버 컴포넌트로 동작하고 있습니다. - 특징:
- 컴포넌트가 서버에서 실행되며 클라이언트 브라우저에서는 실행되지 않습니다.
- Node.js에서 가능한 거의 모든 백엔드 로직을 구현할 수 있습니다.
- 이는 기존에 React를 클라이언트 측에서만 사용하던 것과는 매우 다른 접근 방식입니다.
2. 클라이언트 컴포넌트 (Client Components)
- 브라우저에서 실행되는 컴포넌트입니다.
- 사용해야 하는 경우:
- 브라우저의 로컬 스토리지(LocalStorage) 같은 브라우저 API를 사용해야 할 때.
- 사용자가 컴포넌트와 상호작용할 필요가 있을 때.
- 클라이언트 컴포넌트를 만들기 위해서는 컴포넌트 파일의 맨 위에 아래와 같은 지시문을 추가해야 합니다:
"use client";
정리
- 기본적으로 Next.js의 모든 컴포넌트는 서버 컴포넌트입니다.
- 서버 컴포넌트는 백엔드 로직을 포함할 수 있는 강력한 기능을 제공합니다.
- 클라이언트 컴포넌트는 브라우저에서 동작하며, 브라우저 전용 기능이나 사용자 상호작용이 필요한 경우에 사용됩니다.
14.Next.js에서 클라이언트 및 서버 컴포넌트 설정 -Counter Challenge
서버
import Link from 'next/link'; import React from 'react' const HomePage:React.FC = () => { console.log('Home Page'); return ( <div> <h1 className="text-5xl mb-8 font-bold">Next.js Tutorial</h1> <Link href="/client" className='btn btn-accent'> About Page </Link> </div> ); } export default HomePage;
클라이언트
'use client' import React, { useState } from 'react' const ClientPage:React.FC = () => { const [count, setCount] = useState(0); console.log('Client Page'); return ( <div> <h1 className="text-7xl font-bold mb-10">{count}</h1> <button className='btn btn-primary' onClick={()=>setCount(count+1)}> 증가 </button> </div> ); } export default ClientPage;
1. 홈페이지(HomePage) 구성
- HomePage는 서버 컴포넌트로 동작하며, HTML만 클라이언트에 전달합니다.
- 주요 구성:
- Link를 사용해 /client 페이지로 이동하는 버튼 추가.
- 스타일은 DaisyUI의 btn btn-accent 클래스 사용.
- 서버 컴포넌트로 동작하므로, 브라우저의 콘솔에는 console.log('Home Page')가 나타나지 않고, 서버 로그에서 확인 가능.
2. 클라이언트 페이지(ClientPage) 구성
- ClientPage는 use client 지시어를 사용해 클라이언트 컴포넌트로 설정.
- 주요 구성:
- useState를 활용한 카운터 로직 추가.
- 버튼 클릭 시 count 값을 1씩 증가시키며, DaisyUI 스타일 적용.
- 브라우저 콘솔에서 console.log('Client Page') 확인 가능.
- 클라이언트 컴포넌트로 설정했기 때문에 사용자 상호작용 가능.
3. 클라이언트 컴포넌트와 서버 컴포넌트 차이점
서버 컴포넌트
- JavaScript가 아닌 HTML 결과를 생성해 클라이언트로 전달.
- 브라우저 콘솔이 아닌 서버에서 코드 실행.
- 성능 최적화: JavaScript 로드 없이 빠른 렌더링 제공.
- 상호작용 필요 시 클라이언트 컴포넌트로 분리 필요.
클라이언트 컴포넌트
- 사용자 상호작용이 필요한 경우 설정.
- 브라우저에서 useState 및 기타 React 훅 사용 가능.
- JavaScript가 브라우저에서 실행되므로 동적 동작 가능.
4. 홈페이지와 클라이언트 페이지 연결
- HomePage에서 /client로 연결하는 Link 수정:
- 잘못된 링크(/about) 대신 /client로 수정.
- 버튼 텍스트를 "Get Started"로 변경해 직관성 향상.
5. 실행 과정 요약
HomePage:
- 서버에서 HTML 생성 → 브라우저로 전달.
- 서버 로그에 Home Page 메시지 출력.
ClientPage:
- 브라우저에서 JavaScript 실행.
- useState로 카운터 업데이트 및 동작.
- 브라우저 콘솔에 Client Page 메시지 출력.
6. 중요 개념
- 클라이언트 컴포넌트는 use client 지시어를 반드시 추가해야 상호작용 및 React 훅 사용 가능.
- 서버 컴포넌트에서 클라이언트 컴포넌트를 포함해 혼합 사용 가능:
- 예: 서버 컴포넌트 내에서 상호작용이 필요한 일부 기능만 클라이언트 컴포넌트로 구현.
15.Fetch Data
import Link from 'next/link'; import React from 'react' const url="https://www.thecocktaildb.com/api/json/v1/1/search.php?f=a"; const DrinksPage:React.FC = async () => { const response =await fetch(url); const data =await response.json(); console.log(" DrinksPage :", data); return ( <div> <h1 className="text-2xl mb-10">음료 페이지</h1> <Link href="/" className='text-blue-400'> 홈 </Link> </div> ); } export default DrinksPage;
Next.js에서 서버 컴포넌트의 이점과 데이터 패칭
1. 서버 컴포넌트(Server Component)의 주요 이점
데이터 패칭 최적화:
- 서버 컴포넌트에서는 useEffect와 같은 클라이언트 훅 없이 데이터를 패칭할 수 있음.
- 컴포넌트를 async 함수로 설정하고, fetch, axios 등 원하는 방법으로 데이터 로드 가능.
- 데이터베이스 연결도 서버 컴포넌트에서 바로 처리 가능.
사용자 요청 감소:
- 전통적인 React 앱에서는 클라이언트가 페이지를 로드한 후 추가적인 데이터 요청(HTTP 요청)을 보내야 하지만, 서버 컴포넌트에서는 서버에서 데이터를 미리 패칭하고 HTML에 포함하여 반환.
- 결과적으로 사용자 인터넷 속도와 무관하게 빠른 데이터 로드가 가능.
캐싱 옵션 지원:
- Next.js의 fetch는 기본 fetch를 확장한 형태로, 캐싱 설정 등 추가 기능을 지원.
2. 서버 컴포넌트의 데이터 패칭 예제
다음은 Cocktails DB API를 사용한 데이터 패칭 예제입니다.
서버 컴포넌트 설정:
- 컴포넌트를 async 함수로 선언.
- 전통적인 React에서는 useEffect로 데이터를 패칭했지만, 서버 컴포넌트에서는 불필요.
데이터 요청 로직:
const response = await fetch('https://www.thecocktaildb.com/api/json/v1/1/search.php?f=a'); const data = await response.json(); console.log(data);
- fetch를 사용해 API에서 데이터를 요청.
- JSON 형식으로 데이터를 변환한 후 콘솔에 출력.
출력 확인:
- 서버에서 데이터를 패칭하기 때문에 브라우저 콘솔이 아닌 서버 터미널에 출력 확인.
3. 작동 방식 요약
- 클라이언트가 페이지를 요청하면, 서버에서 데이터를 패칭한 뒤 HTML과 함께 반환.
- 사용자 브라우저는 이미 데이터가 포함된 HTML을 받아 바로 렌더링.
- 데이터가 포함된 HTML을 미리 준비하기 때문에 요청-응답 속도가 더 빠름.
4. 예제 사용 사례: Cocktails DB
Cocktails DB API 사용 이유:
- 알파벳으로 시작하는 음료 데이터를 반환.
- 이미지 데이터를 포함하고 있어, 이후 Next.js의 Image 컴포넌트 사용 시 유용.
https://www.thecocktaildb.com/api/json/v1/1/search.php?f=a
16.Loading Component (로딩 컴포넌트)
loading.js 파일 생성:
- 데이터가 로드되는 동안 보여줄 컴포넌트를 loading.js 파일로 정의.
- 파일 위치는 데이터 패칭을 처리하는 페이지 폴더(/drinks) 내에 위치해야 함.
const loading = () => { return ( <div className="w-full h-screen flex justify-center items-center "> <span className="w-28 loading"> Loading... </span> </div> ); }; export default loading;
데이터 패칭 코드 리팩터링:
- 데이터를 패칭하는 로직을 함수로 분리하여 재사용성을 높임.
- 패칭 속도를 조절하기 위해 setTimeout으로 지연 시간을 추가.
import React from "react"; const url = "https://www.thecocktaildb.com/api/json/v1/1/search.php?f=a"; const fetchDrinks = async () => { await new Promise((resolve) => setTimeout(resolve, 2000)); const response = await fetch(url); if (!response.ok) throw new Error("Failed to fetch data"); return await response.json(); }; const DrinksPage: React.FC = async () => { const data = await fetchDrinks(); console.log(" DrinksPage :", data); return ( <div> <h1 className="text-2xl mb-10">음료 페이지</h1> </div> ); }; export default DrinksPage;
17.Error Component (에러 컴포넌트)
Next.js에서 에러 상태 처리 방법 정리
1. 에러 상태의 중요성
- 서버에서 데이터를 패칭 중 문제가 발생할 가능성 존재.
- 로딩 상태와 마찬가지로 에러 상태를 처리하기 위한 방법 제공.
- Next.js는 error.js 파일을 통해 에러 발생 시 사용자에게 보여줄 내용을 정의 가능.
2. 에러 상태 처리 구성 단계
에러 처리 로직 추가:
- 데이터 패칭 로직에서 response.ok 값을 확인하여 에러 여부 판단.
- response.ok가 false일 경우 에러를 수동으로 발생.
import React from "react"; const url = "https://www.thecocktaildb.com/api/json/v1/1/search.php?f=a"; const fetchDrinks = async () => { await new Promise((resolve) => setTimeout(resolve, 2000)); const response = await fetch(url); if (!response.ok) throw new Error("Failed to fetch data"); return await response.json(); }; const DrinksPage: React.FC = async () => { const data = await fetchDrinks(); console.log(" DrinksPage :", data); return ( <div> <h1 className="text-2xl mb-10">음료 페이지</h1> </div> ); }; export default DrinksPage;
- 404 에러 확인 방법:
- URL을 임의로 잘못 설정하여 테스트.
- 예: 'https://www.thecocktaildb.com/api/json/v1/1/search.php?f=ax'처럼 잘못된 URL 설정.
- 404 에러 확인 방법:
2.error.js 파일 생성:
- 데이터를 패칭하는 페이지 폴더(/drinks)에 error.js 파일 생성.
- 에러 상태를 처리하는 컴포넌트를 작성.
- use client 디렉티브를 반드시 추가 (클라이언트에서 실행되기 때문)
// /drinks/error.js 'use client'; import React from 'react' interface ErrorProps { error: Error; } const ErrorComponent:React.FC<ErrorProps> = ({ error }) => { console.error(error); // 에러 로그 출력 return ( <div> <h1>에러가 발생...</h1> <p>{error.message}</p> </div> ); } export default ErrorComponent;
- 에러 상태 동작 확인:
- 잘못된 URL을 사용하거나 에러를 수동으로 발생시켜 동작 테스트.
- 에러가 발생하면 error.js 컴포넌트가 렌더링되고, 에러 메시지가 표시됨.
- 에러 로그는 콘솔(브라우저 및 터미널)에서 확인 가능.
3. 작동 방식 요약
- **error.js**는 서버 컴포넌트에서 에러 발생 시 자동으로 렌더링.
- use client 디렉티브는 필수로 추가해야 함.
- 발생한 에러 메시지는 error 객체의 message 속성을 통해 접근 가능.
4. 로딩과 에러 처리의 결합
- 로딩과 에러 상태를 함께 처리하여 사용자 경험을 개선.
- 로딩 상태에서는 스피너나 대체 UI를 보여주고, 에러가 발생하면 적절한 메시지 제공.
18.Next.js에서 Nested Layouts(중첩 레이아웃) 설정 방법
1. Nested Layouts란?
- 특정 URL 세그먼트에만 적용되는 레이아웃을 설정하는 방법.
- 루트 레이아웃(app/layout.js)과는 별도로, 특정 페이지 또는 URL 경로 아래의 모든 페이지에만 적용되는 레이아웃을 정의할 수 있음.
2. 중첩 레이아웃 설정 단계
레이아웃 파일 생성:
- layout.js 파일을 특정 폴더(예: /drinks) 안에 생성.
- 해당 폴더의 모든 페이지(/drinks, /drinks/[id] 등)에 공통으로 적용할 레이아웃을 정의.
import React from 'react' interface DrinksLayoutProps { children: React.ReactNode; } const DrinksLayout:React.FC<DrinksLayoutProps> = ({children}) => { return ( <div className='max-w-full'> <div className='mockup-code mb-8'> <pre data-prefix='$'> <code>npx create-next-app@latest nextjs-tutorial</code> </pre> </div> {children} </div> ) } export default DrinksLayout;
- children: 해당 레이아웃 아래에 포함될 모든 하위 콘텐츠를 렌더링.
- 스타일 설정:
- max-w-xl: 레이아웃의 최대 너비를 xl로 제한(루트 레이아웃의 max-w-6xl보다 작음).
- mx-auto: 콘텐츠를 화면 중앙에 정렬.
- mb-4: 하단 마진 추가.
적용 확인:
- /drinks 페이지와 /drinks/[id] 같은 동적 경로에서 레이아웃이 동일하게 적용됨.
- 다른 URL 세그먼트(예: /tasks)에는 해당 레이아웃이 적용되지 않음.
3. 작동 원리
- 루트 레이아웃과 중첩 레이아웃의 관계:
- 루트 레이아웃(app/layout.js)은 모든 페이지에 공통으로 적용.
- 중첩 레이아웃은 특정 세그먼트에만 추가적인 레이아웃을 적용.
- 중첩 레이아웃에서 children 을 반환하지 않으면 하위 콘텐츠가 표시되지 않으므로 반드시 반환해야 함.
4. 구조 요약
- 파일 구조 예
app/ ├── layout.js // 루트 레이아웃 ├── drinks/ │ ├── layout.js // drinks 세그먼트의 중첩 레이아웃 │ ├── page.js // /drinks 페이지 │ ├── [id]/ // 동적 경로 │ │ ├── page.js // /drinks/[id] 페이지
5. 적용 예시
- /drinks 방문 시:
- layout.js에서 정의한 Mockup Code와 콘텐츠 표시.
- 하위 페이지(/drinks/[id])에도 동일 레이아웃 적용.
- Next.js의 중첩 레이아웃은 특정 세그먼트에만 적용되는 고유한 레이아웃을 설정할 수 있는 강력한 기능.
- 레이아웃을 통해 코드 재사용성과 UI 일관성을 높일 수 있음.
19.Next.js에서 Dynamic Routes(동적 라우트) 설정하기
1. Dynamic Routes란?
- 동적 라우트는 특정 경로에 따라 다른 페이지를 렌더링하는 기능.
- 예를 들어, /drinks에 있는 리스트에서 특정 음료를 클릭하면 /drinks/[id] 경로로 이동하며 해당 음료의 정보를 표시.
- 여기서는 id 는 다른 이름으로 변경해도 된다. 여기서는 drinksId 하였다.
2. Dynamic Routes 설정 방법
)동적 폴더 생성:
- [paramName] 형식의 폴더를 생성.
- 예: /app/drinks/[drinksId ]
app/ ├── drinks/ │ ├── [drinksId]/ │ │ ├── page.js
2) page.js 파일 작성:
- 동적 경로로 전달된 파라미터(params)를 사용해 데이터를 가져오거나 렌더링.
- params 객체에서 폴더명과 동일한 이름의 키를 통해 값을 읽을 수 있음.
import React from 'react' interface SingleDrinkPageProps{ params: Promise<{ drinksId: string }>; } const SingleDrinkPage:React.FC<SingleDrinkPageProps> = ({params}) => { console.log('SingleDrinkPage :',params.drinksId); return ( <div>SingleDrinkPage</div> ) } export default SingleDrinkPage;
3. 동작 방식
동적 경로의 역할:
- [id]는 URL의 세그먼트(/drinks/1, /drinks/2 등)를 변수처럼 처리.
- 사용자가 URL에 입력하는 값이 params 객체로 전달됨.
파라미터 사용 예시:
const { id } = params; console.log(id); // 사용자가 입력한 값 출력
URL 접근:
- /drinks/someValue → params.id는 'someValue'.
- /drinks/112 → params.id는 '112'.
4. 파일 구조 예시
app/ ├── layout.js // 루트 레이아웃 ├── drinks/ │ ├── layout.js // drinks 세그먼트의 중첩 레이아웃 │ ├── page.js // /drinks 페이지 │ ├── [drinksId]/ // 동적 경로 폴더 │ │ ├── page.js // /drinks/[id] 페이지
5. 실행 결과
- URL: /drinks/123
- 브라우저 화면
Drink ID: 123 이 페이지는 음료 ID가 123인 데이터를 표시합니다.
콘솔 출력: Drink ID: 123
6. 장점
- 효율적인 페이지 관리:
- 수동으로 페이지를 생성하지 않고도, 공통 템플릿을 사용해 동적으로 다양한 페이지 렌더링 가능.
- 재사용성:
- params를 통해 다양한 데이터를 기반으로 동일한 레이아웃과 컴포넌트를 재사용 가능.
20.Next.js에서 음료 목록과 동적 라우팅 구현
목표:
- 음료 데이터를 기반으로 리스트를 렌더링.
- 각 항목을 클릭하면 해당 음료의 세부 정보 페이지로 이동.
- 동적 라우팅을 활용하여 URL에 따라 정보를 가져와 표시.
타입스크립트 DTO 설정
export interface DrinkDTO { idDrink: string; strDrink: string; strDrinkAlternate: string | null; strTags: string | null; strVideo: string | null; strCategory: string; strIBA: string | null; strAlcoholic: string; strGlass: string; strInstructions: string; strInstructionsES: string | null; strInstructionsDE: string | null; strInstructionsFR: string | null; strInstructionsIT: string | null; strInstructionsZH_HANS: string | null; strInstructionsZH_HANT: string | null; strDrinkThumb: string; strIngredient1: string | null; strIngredient2: string | null; strIngredient3: string | null; strIngredient4: string | null; strIngredient5: string | null; strIngredient6: string | null; strIngredient7: string | null; strIngredient8: string | null; strIngredient9: string | null; strIngredient10: string | null; strIngredient11: string | null; strIngredient12: string | null; strIngredient13: string | null; strIngredient14: string | null; strIngredient15: string | null; strMeasure1: string | null; strMeasure2: string | null; strMeasure3: string | null; strMeasure4: string | null; strMeasure5: string | null; strMeasure6: string | null; strMeasure7: string | null; strMeasure8: string | null; strMeasure9: string | null; strMeasure10: string | null; strMeasure11: string | null; strMeasure12: string | null; strMeasure13: string | null; strMeasure14: string | null; strMeasure15: string | null; strImageSource: string | null; strImageAttribution: string | null; strCreativeCommonsConfirmed: string; dateModified: string | null; } export interface DrinksResponse { drinks: DrinkDTO[]; }
1. DrinksList 컴포넌트
- DrinksList 컴포넌트는 drinks 데이터를 props로 받아와 리스트를 렌더링합니다.
- 각 음료 항목은 <li>로 감싸고, Next.js의 <Link>를 사용하여 세부 페이지로 연결합니다.
코드 분석
import { DrinksResponse } from '@/dto/DrinkDTO'; import Link from 'next/link'; import React from 'react'; const DrinksList: React.FC<DrinksResponse> = ({ drinks }) => { return ( <ul className="menu menu-vertical pl-0"> {drinks.map((drink) => ( <li key={drink.idDrink}> <Link href={`/drinks/${drink.idDrink}`} className="text-xl font-medium"> {drink.strDrink} </Link> </li> ))} </ul> ); }; export default DrinksList;
주요 포인트
drinks 데이터를 props로 받음
- DrinksResponse 타입을 사용해 타입 안전성을 보장.
- drinks.map()를 통해 데이터를 순회.
<ul>과 <li> 사용
- 리스트는 HTML의 <ul>로 구조화.
- Tailwind CSS 클래스(menu, menu-vertical, pl-0)로 스타일 적용.
Next.js의 <Link>
- href 속성에 템플릿 리터럴을 사용하여 동적 경로(/drinks/:id) 생성.
- 음료의 이름(strDrink)을 링크 텍스트로 표시.
2. DrinksPage와 DrinksList 통합
- DrinksPage 컴포넌트에서 API 데이터를 가져오고, 이를 DrinksList 컴포넌트에 전달.
- API 호출은 비동기 함수로 처리되며, 데이터를 정상적으로 로드한 후 리스트를 렌더링.
import DrinksList from "@/components/drinks/DrinksList"; import { DrinksResponse } from "@/dto/DrinkDTO"; import React from "react"; const url = "https://www.thecocktaildb.com/api/json/v1/1/search.php?f=a"; const fetchDrinks = async (): Promise<DrinksResponse> => { const response = await fetch(url); if (!response.ok) throw new Error("Failed to fetch data"); return await response.json(); }; const DrinksPage: React.FC = async () => { const data = await fetchDrinks(); return ( <div> <h1 className="text-2xl mb-10">음료 페이지</h1> <DrinksList drinks={data.drinks} /> </div> ); }; export default DrinksPage;
주요 포인트
fetchDrinks 함수
- API 호출을 통해 음료 데이터를 가져옴.
- 응답 상태가 정상(ok)이 아니면 에러를 발생시킴.
- DrinksResponse 타입으로 반환값을 명확히 지정.
DrinksList와 데이터 통합
- API 응답 데이터 중 drinks 배열을 DrinksList에 전달.
- 리스트 항목 클릭 시 동적으로 생성된 경로(/drinks/:id)로 이동.
3. 동작 흐름
- 사용자가 음료 페이지(/drinks)에 접속.
- fetchDrinks 함수가 API에서 데이터를 가져와 렌더링.
- 각 음료는 리스트로 표시되며, 클릭 시 해당 ID에 따라 세부 페이지(/drinks/:id)로 이동.
21.Next.js에서 음료 상세 페이지 구현
목표:
- 음료 리스트에서 특정 음료를 클릭하면 해당 음료의 세부 정보를 표시.
- URL의 params를 기반으로 음료 데이터를 가져와 렌더링.
- "음료 목록으로 돌아가기" 링크 제공.
1. 음료 상세 페이지 구현 (SingleDrinkPage)
import { DrinkDTO, DrinksResponse } from '@/dto/DrinkDTO'; import Link from 'next/link'; import React from 'react'; import Image from 'next/image'; interface SingleDrinkPageProps { params: Promise<{ drinksId: string }>; } const url = "https://www.thecocktaildb.com/api/json/v1/1/lookup.php?i="; const getSingleDrink = async (drinksId: string) => { const response = await fetch(`${url}${drinksId}`, { cache: 'no-store' }); // 캐싱 방지 if (!response.ok) throw new Error("Failed to fetch data"); const data: DrinksResponse = await response.json(); return data.drinks[0]; }; const SingleDrinkPage = async ({ params }: SingleDrinkPageProps) => { const { drinksId } = await params; const drink =await getSingleDrink(drinksId) as DrinkDTO; if (!drink) { return ( <div> <p>음료 데이터를 가져올 수 없습니다.</p> <Link href="/drinks" className="btn btn-primary mt-8 mb-12"> 음료 목록으로 </Link> </div> ); } const title = drink.strDrink; const imgSrc = drink.strDrinkThumb; return ( <div> <Link href="/drinks" className="btn btn-primary mt-8 mb-12"> 음료 목록으로 </Link> <Image src={imgSrc} alt={title} width={300} height={300} className="w-48 h-48 rounded-lg shadow-lg mb-8" priority /> <h1 className="text-4xl mb-8">{title}</h1> </div> ); }; export default SingleDrinkPage;
2. 주요 구현 내용
getSingleDrink 함수
기능:
URL에서 drinksId를 사용해 특정 음료 데이터를 가져옵니다.
API 응답은 항상 배열로 반환되므로 첫 번째 항목(data.drinks[0])을 반환.코드 설명:
const getSingleDrink = async (drinksId: string) => { const response = await fetch(`${url}${drinksId}`); if (!response.ok) throw new Error("Failed to fetch data"); const data: DrinksResponse = await response.json(); return data.drinks[0]; };
SingleDrinkPage 컴포넌트
기능:
params에서 drinksId를 받아 getSingleDrink를 호출해 데이터를 가져옵니다.
가져온 데이터를 화면에 렌더링합니다.구성 요소:
"음료 목록으로" 링크:
사용자가 음료 리스트 페이지로 쉽게 돌아갈 수 있도록 링크 추가.제목 표시:
API에서 가져온 음료의 이름(strDrink)을 h1 태그로 표시.
코드 설명:
const SingleDrinkPage: React.FC<SingleDrinkPageProps> = async ({ params }) => { const { drinksId } = await params; const drink =await getSingleDrink(drinksId) as DrinkDTO; const title = drink?.strDrink; return ( <div> <Link href='/drinks' className='btn btn-primary mt-8 mb-12'> 음료 목록으로 </Link> <h1 className='text-4xl mb-8'>{title}</h1> </div> ); };
3. 동작 흐름
- 사용자가 음료 리스트 페이지(/drinks)에서 특정 음료를 클릭.
- SingleDrinkPage는 URL의 params.drinksId를 받아 해당 ID로 getSingleDrink를 호출.
- API에서 데이터를 가져오고, 음료 이름과 "돌아가기" 링크를 렌더링.
4. 다음 단계
이미지 추가:
다음 강의에서 Next.js의 <Image> 컴포넌트를 활용해 음료 이미지를 표시.에러 처리:
- 잘못된 ID 요청 시 사용자에게 에러 메시지 표시.
- 로딩 상태를 처리하기 위해 로딩 스피너 추가.
CSS 커스터마이징:
Tailwind CSS를 활용해 UI를 개선.
22.Next.js의 Image 컴포넌트를 사용한 이미지 최적화
1.목표
- 큰 이미지를 최적화하여 사이트의 로딩 속도를 개선.
- Next.js의 Image 컴포넌트를 활용해 레이아웃 시프트(Layout Shift) 문제 해결.
- Tailwind CSS를 활용해 이미지의 크기와 스타일 조정.
2. 기존 이미지 문제점
- 기존 img 태그를 사용하면 큰 이미지를 처리할 때 아래 문제가 발생:
- 느린 로딩 속도: 이미지 파일이 클 경우 로딩 시간이 길어짐.
- 레이아웃 시프트: 이미지를 로드하는 동안 텍스트와 레이아웃이 이동.
3. Next.js Image 컴포넌트의 장점
- 자동 최적화: 이미지를 자동으로 리사이즈하고 적절한 포맷으로 변환.
- 빠른 로딩: 브라우저에 전달되는 이미지의 크기를 줄여 로딩 속도 개선.
- 레이아웃 안정성: 이미지의 높이와 너비를 미리 계산해 레이아웃 시프트 방지.
4. 코드 설명
이미지 파일 준비
- 큰 이미지 다운로드: 원본 크기의 이미지를 다운로드하여 프로젝트 폴더에 추가.
- 파일 경로 설정:
drink.jpg 파일을 컴포넌트와 동일한 디렉토리에 저장.
이미지 가져오기
import drinkImg from './drink.jpg';
- Next.js는 이미지 파일을 가져오면 파일 경로와 크기를 포함한 객체를 반환.
Image 컴포넌트 적용
- 기존 img 태그를 Image 컴포넌트로 대체:
import Image from 'next/image'; <Image src={drinkImg} alt={title} className="w-48 h-48 rounded-lg" />
- 주요 설정:
- src: 이미지 객체(drinkImg) 전달.
- alt: 대체 텍스트로 접근성 개선.
- Tailwind 스타일: w-48, h-48, rounded-lg로 크기와 모서리 스타일 지정.
5. 동작 결과
- 레이아웃 시프트 방지: 이미지 로드 전에 공간이 확보되어 레이아웃이 안정적.
- 최적화된 이미지: 브라우저로 전송되는 이미지 크기가 원본보다 작아져 로딩 속도가 빨라짐.
6. 추가적인 참고 사항
- 이미지 크기 조절: Tailwind 외에도 width와 height 속성으로 직접 조정 가능.
- 리모트 이미지 사용: 원격 URL 이미지를 사용하는 경우 next.config.js에서 도메인을 설정해야 함:
module.exports = { images: { domains: ['example.com'], }, };
접근성 개선: 항상 alt 속성을 설정해 사용자 경험을 향상.
23.Next.js의 Image 컴포넌트를 사용한 원격(Remote) 이미지 처리
1. 원격 이미지 처리의 특징
- 빌드 시 접근 불가: Next.js는 빌드 과정에서 원격 파일에 접근할 수 없기 때문에 몇 가지 추가 설정이 필요.
- 필수 속성: width와 height 속성을 설정해 올바른 종횡비를 유지하고 **레이아웃 시프트(Layout Shift)**를 방지.
- next.config.js 설정 필요: 원격 이미지를 사용하려면 도메인을 next.config.js에 등록해야 함.
- 캐싱 최적화: 서버 컴포넌트로 페이지를 렌더링하면, 첫 요청 후 페이지가 캐싱되어 이후 로딩 속도가 매우 빠름
2. next.config.js 설정
- next.config.js 파일에 images 설정 추가.
- remotePatterns를 사용해 원격 이미지의 호스트, 경로, 프로토콜 등을 정의:
import type { NextConfig } from "next"; const nextConfig: NextConfig = { /* config options here */ images: { remotePatterns: [ { protocol: 'https', hostname: 'www.thecocktaildb.com', pathname: '/images/**', }, ], }, }; export default nextConfig;
서버 재시작: 설정 변경 후 반드시 서버를 재시작해야 적용됨.
3. 원격 이미지 렌더링 코드
import Image from 'next/image'; <Image src={imgSrc} alt={title} width={300} height={300} className="w-48 h-48 rounded-lg shadow-lg mb-8" priority />
주요 포인트
- src: 원격 이미지 URL.
- width & height: 이미지를 올바르게 표시하기 위한 비율 설정.
- 실제 크기 설정은 Tailwind CSS로 진행(w-48, h-48).
- priority: 중요 이미지를 우선적으로 로드.
- className: Tailwind CSS를 활용한 스타일 지정.
- alt: 이미지 대체 텍스트로 접근성 개선.
4. 실행 결과
1) 이미지 최적화
- Next.js는 다양한 크기의 이미지를 준비해 브라우저가 화면에 적합한 크기를 선택하도록 설정.
- srcset 속성을 통해 뷰포트에 따라 적합한 이미지를 제공.
2) 레이아웃 시프트 방지
- width와 height 속성 덕분에 이미지를 로드하기 전에 공간이 확보되어 레이아웃이 안정적.
3) 서버 캐싱
- 서버 컴포넌트를 사용해, 한 번 로드된 페이지는 캐싱되어 이후 재방문 시 즉시 로딩.
5. 원격 이미지 설정이 필요한 이유
- 보안: Next.js는 무분별한 외부 URL 접근을 막기 위해 next.config.js에서 도메인을 명시하도록 요구.
- 성능: 이미지를 최적화해 브라우저에 필요한 최소 용량만 전달.
6. 요약
- Next.js에서 원격 이미지를 사용하려면 next.config.js에 도메인을 등록하고, Image 컴포넌트를 사용해 최적화된 이미지를 제공.
- width, height 속성을 통해 레이아웃 안정성을 확보하고, Tailwind CSS로 크기와 스타일을 조정.
- 서버 캐싱 덕분에 사용자 경험이 향상되고 페이지 로딩 속도가 크게 개선됨.
24.Next.js의 반응형 이미지 처리
1. fill 속성 개념
- fill 속성: 이미지 크기를 부모 요소에 맞게 자동으로 설정할 수 있게 해줍니다. width와 height 값을 하드코딩하지 않고, 부모 요소의 크기를 기반으로 이미지를 크기를 맞추게 됩니다.
- 사용 조건: fill 속성을 사용하려면 부모 요소에 relative 속성을 추가해야 하며, 부모 요소는 기본적으로 block으로 설정됩니다.
2. 코드 리팩토링
import { DrinksResponse } from '@/dto/DrinkDTO' import Image from 'next/image' import Link from 'next/link' import React from 'react' const DrinksList:React.FC<DrinksResponse> = ({drinks}) => { return ( <ul className='grid sm:grid-cols-4 gap-6 mt-6'> { drinks.map((drink) => ( <li key={drink.idDrink}> <Link href={`/drinks/${drink.idDrink}`} className='text-xl font-medium'> <div className='relative h-48 mb-4'> <Image src={drink.strDrinkThumb} fill sizes='(max-width: 768px) 100vw, (max-width: 1200px) 50vw' alt={drink.strDrink} className="rounded-md object-cover" priority /> </div> {drink.strDrink} </Link> </li> )) } </ul> ) } export default DrinksList
3. fill 속성을 사용한 레이아웃 설정
- 그리드 레이아웃: sm:grid-cols-4 를 사용해 작은 화면에서는 4개의 열을 가지는 그리드 레이아웃으로 설정.
- 이미지 부모 요소: 이미지가 fill로 설정되었기 때문에 부모 요소에 relative와 h-48을 설정하여 크기를 조정.
- 이미지 최적화: sizes 속성으로 뷰포트 크기에 따라 이미지를 동적으로 크기 조정.
- 768px 이하에서는 이미지 크기를 100vw로 설정.
- 1200px 이하에서는 이미지 크기를 50vw로 설정.
4. sizes 속성 설명
- sizes 속성은 이미지의 크기를 화면 크기에 맞게 동적으로 설정하는 데 사용됩니다. 예를 들어, 화면 크기가 768px 이하일 때는
- 이미지를 화면 너비 100%로 설정하고, 1200px 이하일 때는 화면 너비의 50%로 설정합니다.
5. 이미지 스타일
- Tailwind CSS: rounded-md, object-cover 클래스를 사용하여 이미지에 둥근 모서리와 커버 모드를 적용.
- object-cover: 이미지가 부모 요소를 완전히 덮도록 설정.
6. 성능 최적화
- priority 속성: 이미지를 우선적으로 로드하여 페이지 로딩 성능을 향상시킴.
- 서버 캐싱: 한 번 페이지가 로드되면 캐싱되어, 이후 방문 시 빠르게 로드됨.
25.Next.js 라우팅 설정 방법 (비공개 폴더, 라우트 그룹, Catch-all 라우트 등)
1. 비공개 폴더 (Private Folders) 설정
- 설정 방법: 비공개 폴더를 만들려면 폴더 이름 앞에 _(언더스코어)를 추가합니다.
- 예시: _/css 폴더는 URL에 포함되지 않으며, 단순히 파일 구조상 폴더로 사용됩니다.
- 이 방법을 사용하면 폴더 내의 파일들을 비공개로 유지할 수 있습니다.
2. 라우트 그룹 (Route Groups) 설정
목적: URL에 영향을 주지 않으면서 라우트를 그룹화할 수 있습니다.
설정 방법: 라우트를 그룹화하려면 폴더 이름을 () 괄호로 감싸면 됩니다.
- 예시: dashboard 폴더를 만들고 그 안에 다른 파일들을 배치할 수 있지만, URL에서 dashboard는 포함되지 않습니다.
// /app/(dashboard)/auth/page.js
테스트: 만약 dashboard 폴더 안에 auth 폴더를 만들면, URL에서 dashboard가 빠진 채로 auth 페이지에 접근할 수 있습니다.
- /auth로 접근할 수 있고, /dashboard/auth는 404 에러를 반환합니다.
3. Catch-all 라우트 설정
목적: 동적 라우트에서 URL의 여러 세그먼트를 처리할 수 있도록 합니다.
설정 방법: [...]를 사용하여 Catch-all 라우트를 설정할 수 있습니다.
- 예시: auth/[sign-in]/page.js 파일을 만들고, URL에서 파라미터를 동적으로 처리할 수 있습니다.
const SignInPage = ({ params }) => { console.log(params); // URL 파라미터 출력 return <h1>Sign In</h1>; };
사용 예시:
- URL: /auth/sign-in -> { signIn: 'sign-in' } 파라미터 출력.
4. Optional Catch-all 라우트
목적: 선택적인 URL 세그먼트를 처리할 수 있습니다.
설정 방법: [[...]]를 사용하여 선택적인 라우트를 설정합니다.
- 예시: auth/[[sign-in]]/page.js에서 sign-in을 선택적으로 처리할 수 있습니다.
테스트: 만약 URL에 /auth/sign-in/users/new처럼 추가 세그먼트가 있으면, 이를 배열 형태로 처리할 수 있습니다.
- 출력: { signIn: 'sign-in', users: 'users', new: 'new' }와 같은 객체가 콘솔에 출력됩니다.
5. 부모 경로 동적 라우트 설정 (두 개의 대괄호)
- 목적: 부모 경로를 포함한 동적 라우트 설정.
- 설정 방법: 두 개의 대괄호 [[...]]를 사용하여 부모 경로를 포함한 동적 라우트를 처리합니다.
- 예시: auth/[[sign-in]]/page.js로 설정하고, 부모 경로 auth가 없다면 기본적으로 404가 아닌 페이지를 렌더링합니다.
6. 결론
- 비공개 폴더와 라우트 그룹: 프로젝트에서 URL을 깔끔하게 유지하면서 라우트 그룹을 설정하거나 비공개 폴더를 사용할 수 있습니다.
- 동적 및 선택적 라우트: 동적 URL 세그먼트를 처리하거나 선택적인 세그먼트를 설정할 수 있어 복잡한 URL 구조를 효율적으로 관리할 수 있습니다.
- Clark 라이브러리 사용: 이번 튜토리얼에서 설명한 기법들은 이후 Clark 인증 라이브러리에서 사용할 수 있으므로 유용한 기법들입니다.
26.Prisma를 사용해 Next.js 프로젝트에 데이터베이스 설정하기
https://www.prisma.io/docs/orm/reference/connection-urls
1. Prisma란?
Prisma는 데이터베이스 상호작용을 간소화해주는 ORM 도구입니다. 개발 단계에서는 SQLite를 사용하여 데이터를 파일로 저장하고, 배포 단계에서는 PlanetScale과 같은 호스팅 서비스를 사용해 MySQL , postsql .. . 로 전환합니다.
prisma는 ORM으로, 객체를 schema로 정의한다음 그 객체와 내가 선택한 데이터 베이스를 연결시켜주는 매개체이다.
server side쪽에서 데이터 베이스를 CRUD가능하도록 해준다.
2. Prisma 확장 프로그램 설치
- VS Code의 Prisma 확장 프로그램 설치 추천:
- 문법 강조(Syntax Highlighting)
- 린팅, 코드 자동완성, 포맷팅 지원 등 개발 편의성 향상.
3. 필수 라이브러리 설치
- Prisma CLI와 Prisma Client 설치
npm install prisma --save-dev npm install @prisma/client
4. Prisma 초기화
- 다음 명령어 실행
npx prisma init
실행 후 프로젝트에 다음이 생성됩니다:
- prisma/schema.prisma: Prisma 설정 파일.
- .env: 데이터베이스 연결 문자열을 포함한 환경변수 파일.
5. 개발용 SQLite 설정
- prisma/schema.prisma 파일에서 provider를 sqlite로 설정
datasource db { provider = "sqlite" url = env("DATABASE_URL") }
.env 파일에 SQLite 파일 경로 설정
DATABASE_URL="file:./dev.db"
6. .env를 Git에서 제외
- .gitignore에 .env 추가
# env files (can opt-in for committing if needed) .env* .env*.local .env*.development .env*.production
7. Prisma Client 연결 관리
Prisma Client는 데이터베이스 연결이 과도하게 생성되지 않도록 설정해야 합니다. 이를 위해 Prisma 공식 문서에서 제공하는 코드를 활용합니다.
- utils/db.ts 파일 생성 후 다음 코드 추가:
import { PrismaClient } from '@prisma/client'; const prismaClientSingleton = () => { return new PrismaClient(); }; type PrismaClientSingleton = ReturnType<typeof prismaClientSingleton>; const globalForPrisma = globalThis as unknown as { prisma: PrismaClientSingleton | undefined; }; const prisma = globalForPrisma.prisma ?? prismaClientSingleton(); export default prisma; if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
8. Prisma Client 사용
- Prisma Client를 직접 가져오지 않고 utils/db.ts에서 만든 prisma를 사용:
import prisma from "@/utils/db"; async function example() { const data = await prisma.exampleTable.findMany(); console.log(data); }
요약
- Prisma로 SQLite를 설정한 후 .env로 환경변수를 관리.
- VS Code 확장을 설치해 개발 편의성 강화.
- 배포 시 SQL로 전환하고 PlanetScale 같은 서비스에 연결.
이 방식은 효율적인 데이터베이스 관리와 안전성을 제공합니다.
27.Prisma를 사용해 Next.js 프로젝트에 첫 번째 모델(Task) 생성 및 데이터베이스 관리하기
1. Task 모델 정의
- prisma/schema.prisma 파일에서 데이터베이스 구조를 설정합니다.
- 예제 Task 모델
model Task { id String @id @default(uuid()) // 기본값 UUID를 사용하는 고유 ID content String // 사용자 입력 내용 createdAt DateTime @default(now()) // 생성 시간, 기본값은 현재 시간 completed Boolean @default(false) // 완료 여부, 기본값은 false }
2. 속성 설명
- id: 기본키로 사용될 고유 식별자(UUID).
- content: 사용자 입력 데이터(문자열).
- createdAt: 데이터 생성 시간(DateTime 타입, 기본값 현재 시간).
- completed: 완료 여부(Boolean 타입, 기본값 false).
3. Prisma 마이그레이션
모델 변경 사항을 데이터베이스에 적용하고 추적하기 위해 마이그레이션을 실행합니다.
마이그레이션 실행:
npx prisma migrate dev --name task_model
- --name 뒤에는 마이그레이션 이름(예: task_model)을 지정합니다.
- 실행 후 prisma/migrations 폴더에 변경 기록이 저장됩니다.
배포 환경에서 PlanetScale을 사용할 경우 명령어가 약간 달라질 수 있습니다.
4. Prisma Studio 실행
Prisma Studio는 데이터베이스를 관리할 수 있는 그래픽 사용자 인터페이스입니다.
- 실행 명령어:
npx prisma studio
- 실행 후 브라우저에서 localhost:5555로 이동.
- 모델(Task)을 클릭하면 데이터베이스에 저장된 항목을 확인하고 관리할 수 있습니다.
5. 요약
- Task 모델을 정의하고 Prisma 마이그레이션을 통해 데이터베이스에 반영.
- Prisma Studio를 사용해 데이터베이스를 쉽게 관리.
- 로컬 환경에서는 SQLite, 배포 시 PlanetScale과 같은 서비스로 전환.
이 과정을 통해 데이터베이스 구조를 간단하게 설정하고 효율적으로 관리할 수 있습니다
Prisma
1 )Prisma 스키마를 수정한 후, 변경 사항을 데이터베이스에 반영 방법
Prisma 스키마를 수정한 후, 변경 사항을 데이터베이스에 반영하려면 다음 단계를 수행해야 합니다:
1. Prisma CLI 명령어 실행
마이그레이션 생성
- Prisma는 데이터베이스 구조를 자동으로 업데이트하지 않습니다. 변경된 스키마를 데이터베이스에 반영하려면 마이그레이션을 생성해야 합니다.
npx prisma migrate dev --name migration_name
- migration_name에는 변경 사항에 대한 설명을 입력(예: add_completed_field).
- 이 명령어는:
- 마이그레이션 파일을 생성.
- 데이터베이스를 업데이트.
- Prisma 클라이언트를 자동 생성.
데이터베이스에 직접 반영하기 (선택사항)
- 마이그레이션 파일 생성 없이 바로 스키마를 데이터베이스에 반영하고 싶다면:
npx prisma db push
이 명령어는 데이터베이스 스키마를 변경하지만, 마이그레이션 기록은 생성하지 않습니다.
2. Prisma Client 재생성
- Prisma 클라이언트는 스키마 변경에 따라 자동으로 업데이트되지만, 수동으로 다시 생성하려면:
npx prisma generate
3. 개발 서버 재시작
- 변경된 Prisma 스키마와 데이터베이스를 반영하려면 서버를 다시 시작해야 합니다:
npm run dev
요약된 명령어 흐름
1) 마이그레이션 생성 및 반영
npx prisma migrate dev --name migration_name
2) Prisma 클라이언트 재생성
npx prisma generate
3)서버 재시작
npm run dev
2) MySQL 데이터베이스를 Prisma에서 사용 할경우 설정 방법
1. Prisma 스키마 수정
schema.prisma 파일에서 데이터베이스 설정을 MySQL로 변경해야 합니다.
// This is your Prisma schema file, // learn more about it in the docs: https://pris.ly/d/prisma-schema // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init generator client { provider = "prisma-client-js" } datasource db { //provider = "postgresql" //provider = "sqlite" provider = "mysql" url = env("DATABASE_URL") shadowDatabaseUrl = env("SHADOW_DATABASE_URL") } model Task{ id String @id @default(uuid()) title String content String createdAt DateTime @default(now()) completed Boolean @default(false) }
2. .env 파일 수정
MySQL 데이터베이스에 연결하기 위해서는 .env 파일에 MySQL 연결 URL을 설정해야 합니다. 아래는 설정 예시입니다.
prisma_shadow 데이터 베이스 생성
create database prisma_shadow CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci; 권한 부여 grant all privileges on prisma_shadow.* to `next_tutorial`@`%` ;
DATABASE_URL="mysql://next_tutorial:1111@localhost:3306/next_tutorial" SHADOW_DATABASE_URL="mysql://next_tutorial:1111@localhost:3306/prisma_shadow"
- next_tutorial는 MySQL 데이터베이스의 유저명.
- 1111은 MySQL 데이터베이스의 비밀번호.
- localhost는 데이터베이스 서버 주소(로컬에서는 localhost).
- 3306은 MySQL의 기본 포트입니다.
- next_tutorial는 MySQL 데이터베이스 이름입니다.
3. MySQL 데이터베이스 설정
- MySQL 서버에 연결하여 데이터베이스 test를 생성합니다. MySQL에서 아래와 같이 명령을 실행하여 데이터베이스와 유저를 설정합니다.
CREATE DATABASE next_tutorial; -- 데이터베이스 생성 CREATE USER 'next_tutorial'@'localhost' IDENTIFIED BY '1111'; -- 유저 생성 GRANT ALL PRIVILEGES ON next_tutorial.* TO 'next_tutorial'@'localhost'; -- 유저 권한 부여 FLUSH PRIVILEGES; -- 권한 적용
4. 마이그레이션 생성 및 데이터베이스 업데이트
MySQL로 데이터베이스를 연결한 후, Prisma를 사용하여 데이터베이스 스키마를 업데이트하려면 마이그레이션을 생성해야 합니다.
기존의 마이그레이션 기록과 관련된 파일을 삭제해야 합니다.
prisma/migrations 디렉토리 삭제:
- 프로젝트의 prisma/migrations 디렉토리를 삭제합니다. 이 디렉토리에는 이전 SQLite 마이그레이션 파일들이 저장되어 있습니다.
rm -rf prisma/migrations
migration_lock.toml 파일 삭제:
- migration_lock.toml 파일도 삭제합니다. 이 파일은 마이그레이션의 잠금 상태를 추적하는 파일입니다.
rm prisma/migration_lock.toml
만약에, db 생성 권한 오류가 발생할경우
shadow database 수동 생성 (선택사항)
만약 위의 권한 부여 후에도 문제가 해결되지 않으면, Prisma에서 사용하는 shadow database를 수동으로 생성할 수 있습니다.
shadow database 생성: MySQL에 접속하여 prisma_shadow라는 이름의 임시 데이터베이스를 생성합니다.
CREATE DATABASE prisma_shadow;
DATABASE_URL 환경 변수 수정: .env 파일에서 DATABASE_URL을 수정하여 shadow database를 명시적으로 지정합니다.
예시:
DATABASE_URL="mysql://next_tutorial:1111@localhost:3306/next_tutorial" SHADOW_DATABASE_URL="mysql://next_tutorial:1111@localhost:3306/prisma_shadow"
shadowDatabaseUrl을 추가하여 Prisma가 prisma_shadow 데이터베이스를 사용할 수 있도록 합니다.
3) Prisma 데이터 손실을 방지하려면 다음을 참고하세요
1. 데이터 손실의 이유
- prisma migrate dev는 개발 환경에서 빠른 반복 작업을 위해 사용됩니다.
- 새로 생성된 마이그레이션 파일이 현재 데이터베이스와 충돌하면 데이터베이스를 초기화하고 스키마를 다시 생성합니다.
2. 데이터를 유지하면서 마이그레이션
다음 방법으로 데이터 손실을 방지할 수 있습니다:
1) 데이터베이스 백업
먼저, 데이터베이스를 백업합니다.
- SQLite: .db 파일을 복사합니다.
- MySQL/PostgreSQL
mysqldump -u [user] -p [database_name] > backup.sql
또는
pg_dump [database_name] > backup.sql
2) 데이터가 있는 상태에서 안전하게 마이그레이션
기존 데이터베이스를 초기화하지 않고 스키마를 업데이트하려면 다음을 사용하세요:
npx prisma migrate deploy
이 명령어는 이미 생성된 마이그레이션을 적용하며, 데이터 손실 없이 마이그레이션을 수행합니다.
3) 모델 변경 시 수동 수정
모델을 수정한 후 기존 데이터를 유지하려면 다음 단계를 수행합니다:
- 새로운 모델을 Prisma 스키마에 추가.
2. 마이그레이션 파일 생성
npx prisma migrate dev --create-only --name task_model
- 3.마이그레이션 파일을 열어 데이터 손실이 없도록 적절히 수정합니다.
- 4.마이그레이션 적용
npx prisma migrate deploy
4) 개발 단계에서는 reset 사용 주의
npx prisma migrate reset은 데이터베이스를 초기화하며, 개발 중 빠르게 초기 상태로 돌아가기 위한 용도입니다. 프로덕션 데이터에는 절대 사용하지 마세요.
3. 개발과 프로덕션의 명령어 구분
개발 환경:
- npx prisma migrate dev: 새로운 마이그레이션을 생성하고 바로 적용.
- 데이터 초기화가 필요하다면 사용 가능
- .
프로덕션 환경:
- npx prisma migrate deploy: 기존 데이터베이스와 스키마를 동기화.
- 데이터 손실 없음.
28.Prisma 기본 CRUD 구현 예제
코드는 Prisma를 사용해 기본 CRUD 기능을 구현하는 예제입니다. 다음은 주요 흐름과 개념에 대한 요약입니다.
타입스크립트 interface
export interface TaskDTO { id: string; content: string; createdAt: Date; completed: boolean; } export interface TasksResponse { tasks: TaskDTO[]; }
1. Prisma Client 설정
import prisma from '@/utils/db';
prisma는 Prisma Client 인스턴스입니다. @/utils/db 경로에서 가져옵니다.
2. Prisma 핸들러 함수
Prisma 관련 작업을 처리하기 위해 prismaHandlers라는 비동기 함수가 정의되었습니다.
주요 작업:
Task 생성:
await prisma.task.create({ data: { content: 'wake up', }, });
- task 모델을 사용해 새 데이터를 생성합니다.
- data 객체에 모델 속성과 값을 지정합니다.
- content 속성만 명시하고 나머지 속성은 Prisma 스키마에서 설정한 기본값을 사용합니다.
2.모든 Task 조회:
const allTasks = await prisma.task.findMany({ orderBy: { createdAt: 'desc', }, });
- findMany 메서드를 사용해 task 모델의 모든 데이터를 가져옵니다.
- orderBy 옵션으로 데이터를 createdAt 필드 기준 내림차순(desc)으로 정렬합니다.
3.결과 반환
return allTasks;
3. React 컴포넌트에서 데이터 렌더링
PrismaExample 컴포넌트는 prismaHandlers에서 가져온 데이터를 화면에 표시합니다.
import React from "react"; import prisma from "@/utils/db"; import { TaskDTO } from "@/dto/TaskDTO"; const prismaHandlers = async () => { console.log("prisma examle"); await prisma.task.create({ data: { content: "일어나세요!", }, }); const allTasks: TaskDTO[] = await prisma.task.findMany({ orderBy: { createdAt: "desc", }, }); return allTasks; }; const PrismaExample: React.FC = async () => { const tasks = await prismaHandlers(); if (tasks.length === 0) { return <h2 className="mt-8 font-medium text-lg">데이터가 없습니다.</h2>; } return ( <div> <h1 className="text-7xl">Prisma 예시 페이지</h1> {tasks.map((task) => { return ( <h2 key={task.id} className="text-xl py-2"> ???? {task.content} </h2> ); })} </div> ); }; export default PrismaExample;
- prismaHandlers에서 데이터를 비동기 호출하고 결과를 tasks 변수에 저장.
- 데이터가 없을 경우 메시지를 출력하고, 있을 경우 반복문(map)으로 데이터를 렌더링.
4. Prisma CRUD 메서드 개념
- Create: 새 레코드를 생성합니다.
prisma.task.create({ data: { content: 'new task' } });
- Find Many: 여러 레코드를 조회합니다.
prisma.task.findMany({ orderBy: { createdAt: 'desc' } });
- Find Unique: 특정 조건에 맞는 단일 레코드를 조회합니다.
prisma.task.findUnique({ where: { id: 'some-id' } });
- Update: 특정 레코드를 업데이트합니다.
prisma.task.update({ where: { id: 'some-id' }, data: { content: 'updated task' }, });
- Upsert: 존재 여부에 따라 업데이트하거나 새로 생성합니다.
prisma.task.upsert({ where: { id: 'some-id' }, create: { content: 'new task' }, update: { content: 'updated task' }, });
- Delete: 특정 레코드를 삭제합니다.
prisma.task.delete({ where: { id: 'some-id' } });
5. React와 Prisma 활용
이 예제는 Prisma CRUD 기능을 이해하고 React 컴포넌트에서 데이터를 처리하고 표시하는 과정을 다룹니다. 향후 서버 액션에 이 코드를 확장하여 더 복잡한 기능을 구현할 수 있습니다.
29.Prisma Task 데이터 목록 표시하기
page.tsx
import TaskForm from '@/components/tasks/TaskForm'; import TaskList from '@/components/tasks/TaskList'; import React from 'react' const TasksPage:React.FC = () => { return ( <div> <h1 className="text-2xl mb-10">작업 페이지</h1> <TaskForm /> <TaskList /> </div> ); } export default TasksPage;
TaskForm.tsx
import React from 'react' const TaskForm = () => { return ( <div>TaskForm</div> ) } export default TaskForm
TaskList.tsx
import React from 'react' import prisma from "@/utils/db"; import { TaskDTO } from '@/dto/TaskDTO'; import Link from 'next/link'; const TaskList = async () => { const tasks : TaskDTO[] =await prisma.task.findMany({ orderBy: { createdAt: "desc", } }) if(tasks.length === 0) { return ( <h2 className='mt-8 font-medium text-lg'>등록된 데이터가 없습니다.</h2> ) } return ( <ul className='mt-8'> {tasks.map((task) => { return ( <li key={task.id} className='flex justify-between items-center px-6 mb-5 py-4 border border-base-300 rounded-lg shadow-lg' > <h2 className={`text-lg capitalize ${task.completed ? "line-through" : null}`}> {task.content} </h2> <div className='flex gap-6 items-center'> <Link href={`/tasks/${task.id}`} className='btn btn-accent btn-xs' > 수정 </Link> </div> </li> ); })} </ul> ) } export default TaskList;
서버 액션으로 데이터 동적 처리
작업 데이터를 서버에서 동적으로 가져오고 표시합니다.
- TaskList는 서버 컴포넌트로 설정되어 Prisma를 통해 직접 데이터베이스 작업을 처리합니다.
- 데이터 가져오기를 상위 페이지에서 하지 않고도, 컴포넌트 내부에서 바로 처리할 수 있습니다.
작업 추가: 이후 구현될 TaskForm을 통해 클라이언트에서 서버 액션으로 데이터를 추가합니다.
작업 삭제: 삭제 폼을 통해 작업을 제거하는 기능도 구현됩니다.
30.Server Actions 개념과 활용: Next.js에서의 비동기 서버 함수
Server Actions란?
- Server Actions는 Next.js에서 제공하는 기능으로, 비동기 서버 함수를 컴포넌트에서 직접 호출할 수 있습니다.
- 기존 방식에서는 API 라우트를 생성하고 요청을 통해 데이터베이스 작업을 처리해야 했지만, Server Actions를 통해 이러한 과정을 간소화할 수 있습니다.
- 이를 통해 컴포넌트 내부에서 바로 데이터베이스 작업(추가, 수정 등)을 수행할 수 있습니다.
기존 방식 vs Server Actions
기존 방식
- 서버 또는 API 라우트(endpoint) 생성.
- 프론트엔드에서 폼을 통해 데이터 제출.
- 서버로 요청 전송 후 데이터베이스 작업 처리.
- 처리 결과를 반환하여 사용자에게 피드백 제공.
Server Actions 방식
- 폼과 Server Action 함수만 필요.
- 요청은 서버로 전송되는 대신 컴포넌트에서 바로 데이터베이스 작업 수행.
- 별도의 API 라우트 없이 데이터를 바로 업데이트하거나 생성 가능.
Server Actions의 규칙
- 함수는 반드시 async로 작성해야 함.
- use server 지시어를 사용해야 함.
- 컴포넌트 내부에 함수 작성 시, 해당 컴포넌트는 서버 컴포넌트여야 함.
- 별도의 파일에서 작성하면 클라이언트 컴포넌트에서도 호출 가능.
- 클라이언트 컴포넌트에서는 Server Action 함수를 정의할 수 없음.
- 별도의 파일에 정의 후 가져와 사용해야 함.
활용 시나리오
- 데이터 추가
- 사용자가 폼에 데이터를 입력하면 서버 액션 함수가 호출되어 데이터베이스에 바로 추가.
- 데이터 수정
- 수정 버튼 클릭 시 서버 액션 함수가 실행되어 데이터베이스의 내용을 업데이트.
- 데이터 삭제
- 삭제 버튼을 눌러 작업 항목을 데이터베이스에서 제거.
예시: Task Form 및 Server Action
다음 단계에서는 Task Form을 설정하고 Server Action 함수를 통해 데이터베이스에 데이터를 추가하는 작업을 구현합니다.
31.Server Actions 활용: Next.js에서 폼과 데이터베이스 통합
Server Actions는 Next.js에서 서버에서 동작하는 비동기 함수를 컴포넌트와 직접 연결할 수 있는 강력한 기능입니다.
아래의 코드는 작업 관리 폼을 작성하고, 사용자 입력을 데이터베이스에 저장하는 기능을 구현하는 예제입니다.
import React from 'react'; import prisma from "@/utils/db"; import { revalidatePath } from 'next/cache'; // 서버에서 동작하는 함수 const createTask = async (formData: FormData) => { 'use server'; // 서버 액션임을 선언 const content = formData.get('content') as string; // 입력된 데이터 가져오기 await prisma.task.create({ // Prisma를 통해 데이터베이스에 추가 data: { content }, }); revalidatePath('/tasks'); // `/tasks` 경로의 정적 페이지를 재검증 }; const TaskForm: React.FC = () => { return ( <form action={createTask}> {/* 서버 액션 함수 호출 */} <div className="join w-full"> <input type="text" className="input input-bordered join-item w-full" placeholder="내용을 입력해 주세요." name="content" required /> <button type="submit" className="btn btn-primary join-item"> 작업 추가 </button> </div> </form> ); }; export default TaskForm;
코드 구성 요소 및 동작 원리
1. 서버 액션 함수(createTask)
- 비동기 함수: async로 선언.
- FormData 파라미터: HTML 폼에서 입력된 데이터가 자동으로 전달.
- 데이터베이스 작업: prisma.task.create를 통해 데이터베이스에 저장.
- 정적 페이지 재검증: revalidatePath('/tasks')를 호출해 변경된 데이터를 반영.
2. TaskForm 컴포넌트
- 폼 요소:
- <form> 태그에 action 속성으로 createTask 연결.
- 입력 필드(<input>): 사용자가 작업 내용을 입력.
- 제출 버튼(<button>): 폼 제출 트리거.
- 스타일링: DaisyUI의 join 클래스를 사용해 레이아웃 정리.
Server Actions의 특징과 동작 방식
1. 기존 방식과의 차이점
2. 핵심 규칙
- 함수는 반드시 **async**로 작성.
- 함수 상단에 'use server'; 지시어 추가.
- 폼의 name 속성으로 입력값을 식별.
- 변경된 정적 페이지를 **revalidatePath**로 업데이트.
동작 과정
- 폼 제출:
- 사용자가 입력값을 제출하면 action 속성에 연결된 createTask 함수가 호출됩니다.
- 서버 작업:
- 서버에서 FormData를 받아 작업 내용을 데이터베이스에 저장합니다.
- 페이지 재검증:
- revalidatePath를 호출해 변경된 데이터를 클라이언트에 즉시 반영합니다.
결과 확인 및 테스트
- Prisma Studio 초기화:
- 기존 작업 데이터를 삭제하고 새 작업을 추가하며 테스트를 준비.
- 브라우저에서 테스트:
- 작업 내용을 입력하고 폼 제출 시 데이터베이스에 저장.
- 페이지가 즉시 갱신되어 변경된 작업 목록을 확인.
장점 요약
- 간소화된 데이터 처리: API 라우트 없이도 서버에서 바로 데이터 작업 가능.
- 페이지 동기화: revalidatePath로 즉각적인 업데이트 가능.
- 사용자 경험 개선: 기본 폼 검증(required)으로 유효하지 않은 입력 방지.
32.Server Actions 활용: Next.js에서 폼과 데이터베이스 통합
데이터 베이스 처리 작업은 actions 디렉토리로 고, 페이지는 app 디렉토리에, 그리고 페이지별 컴포넌트는 components 디렉토리를 따로 만들어
분리하여 개발 진행합니다. 디렉토리명 및 파일 분리 방법은 '마카로닉스' 제 개인적인 개발 진행 방식이니 굳이 똑같이 할 필요는 없습니다.
기능 요약
- Server Actions를 활용하여 작업(Task) 데이터를 생성, 조회, 삭제하는 기능을 구현합니다.
- 데이터 관련 로직을 Actions 파일로 분리하여 코드의 일관성을 유지하고 관리 효율성을 높입니다.
리팩토링 목적
- 코드 일관성: 데이터 관련 함수(getAllTasks, createTask)를 한 파일(tasksActions)로 모아 관리.
- 재사용성: 여러 컴포넌트에서 동일한 데이터를 사용하거나 수정할 때 중복 코드 제거.
- 가독성 향상: 컴포넌트와 비즈니스 로직을 분리하여 코드를 더 깔끔하게 유지.
코드 설명
1. Actions 파일(tasksActions.ts)
- use server 선언: 서버 액션 함수임을 명시.
- getAllTasks:
- 데이터베이스에서 모든 작업을 내림차순으로 정렬하여 가져옵니다.
- createTask:
- 폼 데이터를 통해 작업을 생성하고 /tasks 페이지를 재검증하여 최신 데이터를 즉시 반영합니다.
'use server'; import prisma from "@/utils/db"; import { TaskDTO } from "@/dto/TaskDTO"; import { revalidatePath } from "next/cache"; // 모든 작업 가져오기 export const getAllTasks = async () => { return await prisma.task.findMany({ orderBy: { createdAt: "desc", }, }) as TaskDTO[]; }; // 작업 생성 export const createTask = async (formData: FormData) => { const content = formData.get('content') as string; await prisma.task.create({ data: { content }, }); revalidatePath('/tasks'); // 정적 경로 재검증 };
2. TaskForm 컴포넌트
- 작업 내용을 입력받고 서버 액션 createTask를 호출하여 데이터베이스에 저장합니다.
- 불필요한 로직을 제거하고 tasksActions 파일에서 createTask를 가져와 사용.
import React from 'react'; import { createTask } from '@/actions/tasks/tasksActions'; const TaskForm: React.FC = () => { return ( <form action={createTask}> <div className='join w-full'> <input type="text" className="input input-bordered join-item w-full" placeholder="내용을 입력해 주세요." name="content" required /> <button type='submit' className='btn btn-primary join-item'> 작업 추가 </button> </div> </form> ); }; export default TaskForm;
3. TaskList 컴포넌트
- getAllTasks를 호출해 모든 작업을 가져옵니다.
- 작업 데이터가 없을 경우 "등록된 데이터가 없습니다." 메시지를 출력.
- 작업 데이터를 리스트로 출력하며, 각 항목에 수정 버튼 추가.
import React from 'react'; import { TaskDTO } from '@/dto/TaskDTO'; import Link from 'next/link'; import { getAllTasks } from '@/actions/tasks/tasksActions'; const TaskList = async () => { const tasks: TaskDTO[] = await getAllTasks(); if (tasks.length === 0) { return ( <h2 className='mt-8 font-medium text-lg'>등록된 데이터가 없습니다.</h2> ); } return ( <ul className='mt-8'> {tasks.map((task) => ( <li key={task.id} className='flex justify-between items-center px-6 mb-5 py-4 border border-base-300 rounded-lg shadow-lg' > <h2 className={`text-lg capitalize ${ task.completed ? "line-through" : null }`} > {task.content} </h2> <div className='flex gap-6 items-center'> <Link href={`/tasks/${task.id}`} className='btn btn-accent btn-xs'> 수정 </Link> </div> </li> ))} </ul> ); }; export default TaskList;
리팩토링 후 작업 흐름
1. getAllTasks로 작업 리스트 가져오기
- 데이터 호출 위치 변경:
- 기존에는 TaskList 내부에서 직접 데이터베이스 호출.
- 변경 후 tasksActions.ts 파일의 함수 호출.
2. createTask로 작업 추가
- 폼 데이터 처리 분리:
- TaskForm에서 폼 데이터를 직접 처리하지 않고 tasksActions.ts에서 처리.
다음 단계: 삭제 기능 구현
- deleteTask 함수 추가:
- 작업 삭제 로직을 tasksActions.ts에 추가.
- 삭제 버튼 클릭 시 데이터베이스에서 작업 제거 및 페이지 갱신.
33.삭제 기능 구현 및 정리: Next.js Todo 애플리케이션
기능 요약
- 삭제 기능을 Server Actions로 구현.
- JavaScript가 비활성화된 환경에서도 작동하도록 설계.
- 작업(Task)을 삭제하면 데이터베이스에서 제거되고, 페이지가 즉시 갱신됩니다.
주요 구현 사항
1. 삭제 기능 서버 액션 (deleteTask)
- Server Actions를 사용하여 폼 데이터를 통해 작업 ID를 전달받아 데이터베이스에서 작업을 삭제.
- 작업 삭제 후 revalidatePath('/tasks')로 최신 상태를 즉시 반영.
'use server'; import prisma from "@/utils/db"; import { revalidatePath } from "next/cache"; // 작업 삭제 함수 export const deleteTask = async (formData: FormData) => { const id = formData.get('id') as string; // 폼 데이터에서 ID 추출 await prisma.task.delete({ where: { id, // 삭제할 작업 ID }, }); revalidatePath('/tasks'); // '/tasks' 경로 재검증 };
2. 삭제 버튼 컴포넌트 (DeleteForm)
- 작업 ID를 hidden input으로 서버 액션에 전달.
- 버튼 클릭 시 작업이 삭제되고 화면이 갱신됩니다.
import React from 'react'; import { deleteTask } from '@/actions/tasks/tasksActions'; interface DeleteFormProps { id: string; // 삭제할 작업 ID } const DeleteForm: React.FC<DeleteFormProps> = ({ id }) => { return ( <form action={deleteTask}> {/* Hidden Input으로 ID 전달 */} <input type="hidden" name="id" value={id} /> <button type="submit" className="btn btn-xs btn-error"> 삭제 </button> </form> ); }; export default DeleteForm;
전체 구현 흐름
사용자가 삭제 버튼 클릭:
- DeleteForm 컴포넌트의 <form>이 제출됩니다.
- hidden input으로 작업 ID가 서버로 전달.
서버 액션 호출 (deleteTask):
- formData를 받아 해당 ID의 작업을 데이터베이스에서 삭제.
경로 재검증 (revalidatePath):
- revalidatePath('/tasks')를 호출하여 /tasks 페이지가 최신 데이터로 다시 렌더링.
삭제 기능 테스트
- Prisma Studio에서 작업 리스트 확인.
- 작업 추가 후 삭제 버튼을 눌러 데이터베이스와 동기화 확인.
JavaScript 비활성화 환경 테스트
JavaScript 비활성화 방법:
- 브라우저 개발자 도구(DevTools)에서 JavaScript 비활성화.
테스트 결과:
- 작업 추가 및 삭제가 JavaScript 비활성화 상태에서도 정상 작동.
- 폼 제출 방식으로 동작하기 때문에 JavaScript가 없어도 서버 액션이 작동합니다.
전체 코드
tasksActions.ts
'use server'; import prisma from "@/utils/db"; import { revalidatePath } from "next/cache"; // 모든 작업 가져오기 export const getAllTasks = async () => { return await prisma.task.findMany({ orderBy: { createdAt: "desc", }, }); }; // 작업 생성 export const createTask = async (formData: FormData) => { const content = formData.get('content') as string; await prisma.task.create({ data: { content }, }); revalidatePath('/tasks'); }; // 작업 삭제 export const deleteTask = async (formData: FormData) => { const id = formData.get('id') as string; await prisma.task.delete({ where: { id }, }); revalidatePath('/tasks'); };
DeleteForm.tsx
import React from 'react'; import { deleteTask } from '@/actions/tasks/tasksActions'; interface DeleteFormProps { id: string; } const DeleteForm: React.FC<DeleteFormProps> = ({ id }) => { return ( <form action={deleteTask}> <input type="hidden" name="id" value={id} /> <button type="submit" className="btn btn-xs btn-error"> 삭제 </button> </form> ); }; export default DeleteForm;
결론
- Next.js의 Server Actions를 활용해 삭제 기능을 간단하고 효율적으로 구현.
- JavaScript 비활성화 상태에서도 완벽히 작동.
- 장점:
- 간결한 코드: 데이터 로직과 컴포넌트를 분리하여 유지보수성 증가.
- 서버 렌더링: 최신 데이터가 즉시 반영.
- 접근성 향상: 폼 기반 방식으로 모든 환경에서 동작.
34.Next.js 수정하기 구현
1. 작업 개요
위 코드는 Next.js와 Prisma, React를 활용하여 작업(Task) 데이터를 편집할 수 있는 기능을 구현하는 내용을 다룹니다.
주요 흐름은 다음과 같습니다:
- 단일 작업 데이터를 가져오기
- 작업 수정 폼(EditForm) 렌더링
- 작업 수정 처리 및 데이터 업데이트
- 수정 후 리다이렉션 처리
2. 주요 구성 요소
(1) EditForm 컴포넌트
작업 데이터를 편집할 수 있는 입력 폼을 제공합니다.
task 데이터를 받아와 폼에 기본값으로 설정하며, 사용자가 이를 수정할 수 있습니다.
코드 구성:
- 숨겨진 필드:
- 작업 ID를 보이지 않게 전달 (<input type="hidden" name="id" />).
- 내용 필드:
- 작업 내용(content)을 입력하는 텍스트 필드.
- 기본값은 작업 데이터에서 가져옵니다.
- 완료 체크박스:
- 작업 완료 여부를 나타내며, 기본값은 DB 데이터(completed)와 동기화.
- 제출 버튼:
- 작업 데이터를 서버로 제출해 업데이트를 처리.
(2) SingleTaskPage 컴포넌트
단일 작업(Task)의 세부 정보를 보여주고 수정할 수 있는 페이지입니다.
- getTask: 작업 ID로 DB에서 작업 데이터를 조회.
- EditForm 컴포넌트: 작업 데이터를 편집할 수 있는 폼 렌더링.
- Link 컴포넌트: 목록 페이지로 이동할 수 있는 버튼 제공.
/src/app/tasks/[taskId]/page.tsx
import { getTask } from '@/actions/tasks/tasksActions' import EditForm from '@/components/tasks/EditForm'; import { TaskDTO } from '@/dto/TaskDTO'; import Link from 'next/link'; import React from 'react' interface SingleTaskPageProps{ params:Promise<{taskId:string}>; } const SingleTaskPage:React.FC<SingleTaskPageProps> =async ({params}) => { const {taskId} = await params; const task =await getTask(taskId) as TaskDTO; return ( <> <div className='mb-16'> <Link href="/tasks" className='btn btn-accent mb-5'> 목록 화면 </Link> </div> <EditForm task={task } /> </> ) } export default SingleTaskPage
EditForm.tsx
import React from 'react'; import { editTask } from '@/actions/tasks/tasksActions'; import { TaskDTO } from '@/dto/TaskDTO'; interface EditFormProps { task: TaskDTO; } const EditForm: React.FC<EditFormProps> = ({ task }) => { const { id, completed, content } = task; return ( <form action={editTask} className="max-w-full mx-auto mt-8 bg-white shadow-md rounded-lg p-8"> <h2 className="text-2xl font-semibold mb-6 text-gray-700">작업 수정</h2> <input type="hidden" name="id" value={id} /> {/* Content Input */} <div className="mb-6"> <label htmlFor="content" className="block text-sm font-medium text-gray-600 mb-2"> 내용 </label> <input type="text" id="content" className="input input-bordered w-full text-sm p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:outline-none" placeholder="내용을 입력해 주세요." name="content" required defaultValue={content} /> </div> {/* Completed Checkbox */} <div className="mb-6"> <label htmlFor="completed" className="flex items-center cursor-pointer"> <span className="mr-3 text-sm font-medium text-gray-600">완료</span> <input type="checkbox" id="completed" name="completed" className="checkbox checkbox-primary" defaultChecked={completed} /> </label> </div> {/* Submit Button */} <button type="submit" className="btn btn-primary w-full py-2 text-white font-medium text-lg tracking-wide hover:bg-blue-600 transition duration-200 rounded-lg" > 작업 수정 </button> </form> ); }; export default EditForm;
3. 서버 액션 (Server Actions)
Next.js에서 서버에서 동작하는 두 가지 주요 함수가 사용됩니다.
(1) getTask 함수
Prisma ORM을 사용해 단일 작업 데이터를 DB에서 가져옵니다.
export const getTask = async (id: string) => { return await prisma.task.findUnique({ where: { id } }) as TaskDTO; };
(2) editTask 함수
제출된 폼 데이터를 기반으로 작업을 업데이트합니다.
- 폼 데이터 처리:
- formData.get("id"), formData.get("content"), formData.get("completed")로 데이터 추출.
- Prisma를 통한 업데이트:
- Prisma.task.update를 사용해 DB 데이터를 업데이트.
- 체크박스 값(completed)은 "on"이면 true로 변환.
- 페이지 갱신 및 리다이렉션:
- revalidatePath('/tasks')로 캐시된 페이지를 갱신.
- redirect('/tasks')로 수정 후 작업 목록 페이지로 리다이렉션.
4. 구현 흐름
(1) 작업 데이터 조회
- 페이지가 렌더링되면 params에서 작업 ID를 추출.
- getTask 함수로 DB에서 작업 데이터를 가져옵니다.
(2) 작업 수정 폼 렌더링
- EditForm 컴포넌트에 작업 데이터를 전달해 기본값 설정.
- 사용자는 내용을 수정하거나 완료 여부를 체크할 수 있습니다.
(3) 작업 수정 제출
- 폼 데이터를 editTask 함수로 제출.
- DB 데이터를 업데이트한 뒤 작업 목록 페이지로 리다이렉션.
5. 주요 코드 개선
(1) UI 개선
폼과 버튼의 스타일을 다듬어 사용자 경험을 개선:
- Tailwind CSS로 간결하고 직관적인 디자인 적용.
- 버튼에 hover 효과와 부드러운 모서리 추가.
- 입력 필드와 라벨의 간격 및 배치를 정돈.
(2) 에러 처리
- Prisma 작업 처리 시 try-catch 블록을 사용해 에러를 핸들링.
- redirect는 try 블록 외부에 배치해 실행 중단 방지.
6. 전체 코드 요약
- SingleTaskPage:
- 작업 데이터를 가져와 수정 폼 렌더링.
- EditForm:
- 작업 수정 UI 제공 및 폼 제출 처리.
- getTask:
- Prisma로 단일 작업 데이터 조회.
- editTask:
- 폼 데이터를 기반으로 작업 업데이트 및 페이지 리다이렉션.
7. 결과
- 작업 목록 페이지에서 작업을 선택하면 수정 페이지로 이동.
- 작업 수정 후 DB에 업데이트되고 작업 목록 페이지로 돌아감.
- 직관적이고 사용자가 편리하게 작업을 수정할 수 있는 기능 제공.
35.Pending State 설명 및 코드 기반 상세 설명
1. Pending State란?
Pending State는 비동기 작업(예: 서버 요청) 중에 사용자가 현재 작업 상태를 알 수 있도록 만들어진 상태를 말합니다.
애플리케이션에서 중요한 점은 사용자가 작업 처리 중임을 인지할 수 있도록 UI에 피드백을 주는 것입니다.
예를 들어:
- 폼을 제출했을 때 버튼을 비활성화하거나,
- "처리 중..." 같은 메시지를 보여줌으로써 작업 진행 중임을 알립니다.
2. 코드 흐름과 Pending State 구현
구성요소
이 코드는 사용자가 작업(Task)을 추가하는 기능을 중심으로 작성되었으며, 폼 제출 중 로딩 상태를 관리하기 위해 Pending State를 적용했습니다.
주요 컴포넌트 및 함수:
- createTaskCustom:
- 비동기로 작업을 추가하는 함수.
- 작업 처리 중 Promise를 사용해 테스트용 딜레이를 추가.
- SubmitBtn:
- **폼의 제출 상태(pending)**를 감지하고 UI를 업데이트.
- 버튼의 상태(활성/비활성)와 텍스트 변경.
- TaskFormCustom:
- 작업을 입력하고 제출하는 폼.
- SubmitBtn과 함께 동작해 Pending State를 사용자에게 표시.
3. 코드 상세 설명
(1) createTaskCustom 함수
export const createTaskCustom = async (formData: FormData) => { // 테스트를 위한 1초 지연 await new Promise((resolve) => setTimeout(resolve, 1000)); const content = formData.get('content') as string; // Prisma를 사용해 데이터베이스에 작업 추가 await prisma.task.create({ data: { content }, }); // 작업 목록 페이지 갱신 revalidatePath('/tasks'); };
설명:
- 비동기 작업:
- await를 통해 작업이 완료될 때까지 코드가 대기.
- 테스트용으로 Promise로 1초 딜레이를 추가(setTimeout).
- 작업 데이터 저장:
- Prisma ORM을 사용해 데이터베이스에 작업(content) 추가.
- 페이지 갱신:
- revalidatePath('/tasks')로 작업 목록 페이지를 새로고침.
(2) SubmitBtn 컴포넌트
'use client'; import React from 'react'; import { useFormStatus } from 'react-dom'; const SubmitBtn = () => { const { pending } = useFormStatus(); return ( <button type="submit" className="btn btn-primary join-item" disabled={pending}> {pending ? '처리 중...' : '작업 추가'} </button> ); }; export default SubmitBtn;
설명:
- useFormStatus:
- React의 서버 액션 상태 관리 훅.
- 현재 폼이 제출 중(pending)인지 여부를 반환.
- 버튼 동작:
- pending이 true일 때:
- 버튼 비활성화(disabled).
- 텍스트: "처리 중...".
- pending이 false일 때:
- 버튼 활성화.
- 텍스트: "작업 추가".
- pending이 true일 때:
- 사용자 피드백:
- 작업 처리 중 중복 제출을 방지.
- 로딩 상태를 명확히 전달.
(3) TaskFormCustom 컴포넌트
import React from 'react'; import { createTaskCustom } from '@/actions/tasks/tasksActions'; import SubmitBtn from './SubmitBtn'; const TaskFormCustom: React.FC = () => { return ( <form action={createTaskCustom}> <div className="join w-full"> {/* 작업 내용 입력 필드 */} <input type="text" className="input input-bordered join-item w-full" placeholder="내용을 입력해 주세요." name="content" required /> {/* 제출 버튼 */} <SubmitBtn /> </div> </form> ); }; export default TaskFormCustom;
- 폼 구성:
- 사용자가 작업 내용을 입력(name="content")하고 제출.
- SubmitBtn 통합:
- 제출 버튼을 포함해 Pending State를 감지하고 사용자에게 전달.
- SubmitBtn은 작업 처리 상태에 따라 UI를 업데이트.
4. 동작 과정
(1) 폼 제출
- 사용자가 작업 내용을 입력하고 제출 버튼을 클릭.
- createTaskCustom 서버 액션 호출:
- 작업 데이터를 가져옴.
- 1초 지연(테스트용) 후, DB에 작업 추가.
- 작업 목록 페이지 갱신.
(2) Pending State 관리
- 로딩 상태 확인:
- useFormStatus에서 pending 값 확인.
- 제출 중(pending=true)이면:
- 버튼 비활성화(disabled).
- 텍스트: "처리 중...".
- 로딩 완료 후 복구:
- 작업 완료 시:
- 버튼 다시 활성화.
- 텍스트: "작업 추가".
- 작업 완료 시:
36.Pending 상태와 오류 처리 및 TaskFormCustom의 개선 과정
1. 문제 정의 및 목표
현재 상태 문제:
- 기존의 작업(Task)을 추가하는 기능은 동작하지만, 사용자에게 명확한 피드백이 부족합니다.
- 작업이 성공적으로 처리되었는지, 오류가 발생했는지에 대한 정보를 제공하지 않습니다.
목표:
- 작업 처리 상태(성공/실패)를 사용자에게 명확히 전달.
- 오류가 발생하면 이를 화면에 표시.
- 비동기 작업 중 UI가 적절히 동작하도록 개선.
2. 주요 구현 내용
(1) createTaskCustom 함수에서의 오류 처리
createTaskCustom 함수는 서버에서 작업을 처리하고, 성공 여부나 오류 메시지를 반환합니다.
기존 문제점:
- 오류를 잡아내지 못하거나, 처리 결과를 사용자에게 알리지 않음.
개선:
- try-catch 블록을 추가해 오류를 처리.
- 작업이 성공했을 때와 실패했을 때 각각 메시지를 반환.
(2) TaskFormCustom에서 사용자 피드백
- 사용자 경험을 개선하기 위해 폼 상태 관리를 추가.
- 성공/실패 메시지를 화면에 표시.
- useFormState 훅을 사용해 상태와 동작을 제어.
3. 코드 상세 설명
(1) createTaskCustom 함수
export const createTaskCustom = async ( prevState: PrevState, formData: FormData ): Promise<CreateTaskResponse> => { const content = formData.get('content') as string; try { // Prisma를 사용해 작업 추가 await prisma.task.create({ data: { content }, }); // 작업 목록 페이지 갱신 revalidatePath('/tasks'); // 성공 메시지 반환 return { message: 'success!!!' }; } catch (error) { console.error('*** Error creating task:', error); // 실패 메시지 반환 return { message: 'error...' }; } };
- 구현 내용:
- 작업 추가: Prisma ORM을 통해 DB에 새로운 작업을 추가.
- 오류 처리: try-catch로 작업 중 발생한 오류를 잡아내고, 사용자에게 메시지로 반환.
- 성공/실패 메시지 반환:
- 작업 성공 시 { message: 'success!!!' }.
- 작업 실패 시 { message: 'error...' }.
(2) TaskFormCustom 컴포넌트
'use client'; import React from 'react'; import { createTaskCustom } from '@/actions/tasks/tasksActions'; import { useFormState, useFormStatus } from 'react-dom'; const SubmitBtn = () => { const { pending } = useFormStatus(); return ( <button type="submit" className="btn btn-primary join-item" disabled={pending}> {pending ? '처리중...' : '작업 추가'} </button> ); }; interface FormState { message: string | null; } const initialState: FormState = { message: null, }; const TaskFormCustom: React.FC = () => { const [state, formAction] = useFormState(createTaskCustom, initialState); return ( <form action={formAction}> {state.message ? <p className="mb-2">{state.message}</p> : null} <div className="join w-full"> <input type="text" className="input input-bordered join-item w-full" placeholder="내용을 입력해 주세요." name="content" required /> <SubmitBtn /> </div> </form> ); }; export default TaskFormCustom;
=========>useActionState 업데이트
useFormState , useActionState 는
1) 클라이언트 에서 만 적용가능한 코드로 'use client' 로 설정해야 합니다.
2) 매개변수로 prevState를 필수적으로 필요로 합니다.
'use client' import React, { useEffect } from 'react' import { createTaskCustom, CreateTaskResponse } from '@/actions/tasks/tasksActions'; import { useActionState } from "react"; import toast from 'react-hot-toast'; interface SubmitBtnProps { isPending:boolean; } const SubmitBtn:React.FC<SubmitBtnProps>=({isPending })=>{ return ( <button type='submit' className='btn btn-primary join-item' disabled={isPending} > {isPending? '처리중...' : '작업 추가' } </button> ) } const initialState: CreateTaskResponse = { errors : {}, // 초기에는 에러 없음 message: null, }; const TaskFormCustom:React.FC = () => { const [state, formAction, isPending]= useActionState(createTaskCustom, initialState); useEffect(()=>{ if(state.message){ if(state.message==="success"){ toast.success("성공적으로 등록 처리 되었습니다."); return; }else{ toast.error(state.message); return; } } },[state]); return ( <form action={formAction}> {/* 성공 메시지 */} {/* {state.message && ( <p className={`mb-2 ${ state.message.includes('성공') ? 'text-green-500' : 'text-red-500' }`} > {state.message} </p> )} */} <div className="w-full mb-4 flex flex-row justify-between"> {/* Title 입력 */} {/* <div className="mb-4"> <input type="text" name="title" className="input input-bordered join-item w-full" placeholder="제목을 입력해 주세요." required /> {state.errors && state.errors.title && ( <p className="text-red-500 text-sm mt-1">{state.errors.title}</p> )} </div> */} {/* Content 입력 */} <div className="mb-4 w-4/5"> <input type="text" name="content" className="input input-bordered join-item w-full" placeholder="내용을 입력해 주세요." required /> {state.errors &&state.errors.content && ( <p className="text-red-500 text-sm mt-1">{state.errors.content}</p> )} </div> <SubmitBtn isPending={isPending}/> </div> </form> ) } export default TaskFormCustom;
4. 주요 구현 요소 설명
(1) useFormState 사용
- React의 새로운 훅으로, 폼 상태와 폼 동작을 관리.
- 반환값:
- 상태(state):
- message: 성공 또는 실패 메시지를 관리.
- 폼 액션(formAction):
- 서버 액션(createTaskCustom)을 호출하는 함수.
- 상태(state):
(2) 상태 관리 (state.message)
- 폼 제출 결과(성공/실패 메시지)를 화면에 표시.
- state.message가 null이 아니면 메시지를 출력:
{state.message ? <p className="mb-2">{state.message}</p> : null}
(3) 버튼 비활성화 (SubmitBtn)
- useFormStatus 훅을 사용해 폼 제출 상태(pending)를 확인.
- 폼 제출 중에는 버튼을 비활성화하고, "처리중..." 텍스트를 표시.
5. 동작 과정
(1) 폼 제출
- 사용자가 작업 내용을 입력하고 제출 버튼을 클릭.
- formAction 호출 → createTaskCustom 실행.
- createTaskCustom에서 작업 처리:
- 작업이 성공하면 message: 'success!!!' 반환.
- 작업이 실패하면 message: 'error...' 반환.
(2) 상태 업데이트
- useFormState를 통해 state가 업데이트.
- state.message 값에 따라 성공/실패 메시지를 화면에 출력.
(3) 로딩 상태 표시
- 작업 처리 중(pending=true):
- 버튼이 비활성화.
- 텍스트: "처리중...".
37.Zod 라이브러리 설치 및 유효성 체크 과정
1. Zod 라이브러리 설치
우선, 유효성 검사 라이브러리인 Zod를 프로젝트에 설치해야 합니다. Zod는 타입 안전한 스키마 유효성 검사를 제공하며, 다양한 유효성 검사 기능을 손쉽게 구현할 수 있게 해줍니다.
npm install zod
설치가 완료되면, zod에서 필요한 기능을 가져와 사용할 수 있습니다.
2. Zod 라이브러리 사용 및 유효성 체크 설정
Zod를 사용하여 입력값을 검사하려면, 먼저 스키마를 정의해야 합니다. 이 스키마는 유효성 검사를 수행할 데이터를 어떻게 검사할지 규정합니다.
import { z } from 'zod'; // Zod 스키마 정의 const Task = z.object({ content: z.string() // content 필드는 문자열이어야 함 .min(5, '내용은 5글자보다 더 커야 합니다.') // 최소 5글자 이상이어야 함 .max(30, '내용은 최대 30글자 이햐여아 합니다.') // 최대 30글자 이내여야 함 });
여기서, z.object() 메서드를 사용하여 content 필드를 검사하는 스키마를 정의했습니다. 이 필드는 문자열이어야 하며, 최소 5글자 이상, 최대 30글자 이내로 입력값을 제한합니다.
3. 유효성 검사 실행 및 에러 처리
유효성 검사는 Task.parse() 메서드를 사용하여 수행합니다. 이 메서드는 유효성 검사에 실패할 경우 ZodError를 발생시키며, 이를 통해 에러 메시지를 추출할 수 있습니다.
try { // 유효성 검사 Task.parse({ content }); // 유효성 검사 통과 후 DB 작업 await prisma.task.create({ data: { content } }); revalidatePath('/tasks'); return { message: "성공적으로 등록 처리되었습니다.", errors: {} }; } catch (error) { console.error('*** Error creating task:', error); // Zod 에러 처리 if (error instanceof z.ZodError) { const fieldErrors: Record<string, string> = {}; // Zod 에러를 순회하여 각 필드에 맞는 에러 메시지 반환 error.errors.forEach((err) => { if (err.path[0]) { fieldErrors[err.path[0]] = err.message; } }); return { message: '등록형식에 맞지 않습니다.', errors: fieldErrors }; } // 기타 예기치 않은 에러 처리 return { message: 'Task를 생성하는 동안 예기치 않은 오류가 발생했습니다.', errors: {} }; }
이 코드에서는 Task.parse({ content })를 호출하여 유효성 검사를 수행하고, 유효성 검사를 통과하면 DB에 데이터를 저장합니다. 만약 유효성 검사에서 오류가 발생하면, ZodError가 발생하며, 이를 캐치하여 각 필드에 대한 오류 메시지를 errors 객체에 담아서 반환합니다.
4. 에러 메시지 처리 및 반환
ZodError를 캐치하면, error.errors 배열을 통해 각 필드별 오류 메시지를 가져올 수 있습니다. 이를 fieldErrors 객체에 저장하여, 각 필드에 맞는 에러 메시지를 반환합니다.
if (error instanceof z.ZodError) { const fieldErrors: Record<string, string> = {}; // 각 필드별 에러 메시지 추출 error.errors.forEach((err) => { if (err.path[0]) { fieldErrors[err.path[0]] = err.message; } }); return { message: '등록형식에 맞지 않습니다.', errors: fieldErrors }; }
fieldErrors는 객체 형태로 각 필드명과 해당 필드에 대한 에러 메시지를 저장합니다. 예를 들어, content 필드에 대한 오류 메시지가 있으면 fieldErrors.content에 메시지가 들어가게 됩니다.
5. 클라이언트 사이드에서 에러 메시지 표시
클라이언트에서 useFormState를 사용하여 상태를 관리하고, 상태에 따라 에러 메시지를 표시합니다.
state.errors에 저장된 각 필드의 오류 메시지를 출력합니다.
<div className="mb-4 w-4/5"> <input type="text" name="content" className="input input-bordered join-item w-full" placeholder="내용을 입력해 주세요." required /> {state.errors && state.errors.content && ( <p className="text-red-500 text-sm mt-1">{state.errors.content}</p> )} </div>
위 코드에서 state.errors.content는 createTaskCustom 함수에서 발생한 content 필드의 오류 메시지를 표시합니다.
6. 유효성 검사를 위한 주요 기능 요약
- Zod 라이브러리를 사용하여 스키마를 정의하고, 유효성 검사를 수행합니다.
- 유효성 검사 실패 시, **ZodError**를 사용하여 각 필드에 대한 에러 메시지를 추출합니다.
- 에러 메시지는 errors 객체에 필드별로 저장되고, 클라이언트에서 이를 화면에 출력합니다.
이와 같은 방식으로 Zod를 활용하여 강력한 유효성 검사를 구현할 수 있으며, 유효성 검사 후에 사용자에게 적절한 에러 메시지를 반환하여 더 나은 사용자 경험을 제공할 수 있습니다.
38.Providers 설정 후 toast 전역에 사용 방법
1. Providers 설정
Providers는 전역적으로 적용할 기능이나 라이브러리를 설정하는 컴포넌트입니다. 예를 들어, React Hot Toast, Redux Toolkit, React Query 등과 같은 전역 상태 관리나 알림 라이브러리를 설정하는 데 사용됩니다. 이 컴포넌트는 모든 페이지와 컴포넌트를 감싸는 구조로 사용되며, 앱 레이아웃에 적용합니다.
Providers 컴포넌트 코드
src/app/providers.tsx
'use client'; import React from 'react'; import { Toaster } from 'react-hot-toast'; interface ProvidersProps { children: React.ReactNode; } const Providers: React.FC<ProvidersProps> = ({ children }) => { return ( <> <Toaster /> {/* React Hot Toast 글로벌 컴포넌트 */} {children} {/* 자식 컴포넌트들 */} </> ); }; export default Providers;
설명
Toaster 추가:
- react-hot-toast 라이브러리에서 제공하는 전역 알림 컴포넌트입니다.
- 모든 화면에서 알림을 사용할 수 있도록 Providers 안에 추가합니다.
children:
- 이 컴포넌트는 레이아웃에서 감싸는 모든 자식 컴포넌트를 받아서 반환합니다.
use client:
- Next.js의 서버 컴포넌트와 클라이언트 컴포넌트를 구분하기 위해 선언합니다.
- react-hot-toast는 클라이언트에서 실행되어야 하므로 use client가 필요합니다.
2. RootLayout에 Providers 적용
전역 레이아웃 파일(RootLayout)에서 Providers를 감싸는 방식으로 적용합니다.
3. toast를 이용한 알림 처리 사용하
toast를 사용해 작업 성공, 오류 등의 알림을 사용자에게 제공합니다. 예를 들어, createTaskCustom 함수에서 작업 결과를 알림으로 표시할 수 있습니다.
알림 처리 코드
'use client' import React, { useEffect } from 'react' import { createTaskCustom, CreateTaskResponse } from '@/actions/tasks/tasksActions'; import { useActionState } from "react"; import toast from 'react-hot-toast'; interface SubmitBtnProps { isPending:boolean; } const SubmitBtn:React.FC<SubmitBtnProps>=({isPending })=>{ return ( <button type='submit' className='btn btn-primary join-item' disabled={isPending} > {isPending? '처리중...' : '작업 추가' } </button> ) } const initialState: CreateTaskResponse = { errors : {}, // 초기에는 에러 없음 message: null, }; const TaskFormCustom:React.FC = () => { const [state, formAction, isPending]= useActionState(createTaskCustom, initialState); useEffect(()=>{ if(state.message){ if(state.message==="success"){ toast.success("성공적으로 등록 처리 되었습니다."); return; }else{ toast.error(state.message); return; } } },[state]); return ( <form action={formAction}> {/* 성공 메시지 */} {/* {state.message && ( <p className={`mb-2 ${ state.message.includes('성공') ? 'text-green-500' : 'text-red-500' }`} > {state.message} </p> )} */} <div className="w-full mb-4 flex flex-row justify-between"> {/* Title 입력 */} {/* <div className="mb-4"> <input type="text" name="title" className="input input-bordered join-item w-full" placeholder="제목을 입력해 주세요." required /> {state.errors && state.errors.title && ( <p className="text-red-500 text-sm mt-1">{state.errors.title}</p> )} </div> */} {/* Content 입력 */} <div className="mb-4 w-4/5"> <input type="text" name="content" className="input input-bordered join-item w-full" placeholder="내용을 입력해 주세요." required /> {state.errors &&state.errors.content && ( <p className="text-red-500 text-sm mt-1">{state.errors.content}</p> )} </div> <SubmitBtn isPending={isPending}/> </div> </form> ) } export default TaskFormCustom;
설명
useEffect로 상태 변화 감지:
- state.message가 변경될 때마다 실행됩니다.
- 메시지 값이 success면 성공 알림(toast.success), error면 오류 알림(toast.error)을 표시합니다.
toast.success와 toast.error:
- react-hot-toast 라이브러리에서 제공하는 함수로, 사용자에게 시각적으로 알림을 제공합니다.
39.useFormStatus를 사용한 Delete 버튼 interactivity 구현
1. 기능 설명
- 삭제 버튼에 "삭제 중..." 상태를 표시하고, 버튼이 비활성화되도록 처리합니다.
- useFormStatus 훅을 활용하여 폼의 상태(예: 제출 진행 중)를 관리합니다.
- 버튼을 별도의 컴포넌트(SubmitButton)로 분리하여 재사용성을 높이고 코드를 간결하게 유지합니다.
import React from 'react' import { deleteTask } from '@/actions/tasks/tasksActions'; interface DeleteFormProps { id:string } const DeleteForm:React.FC<DeleteFormProps> = ({id}) => { return ( <form action={deleteTask}> <input type='hidden' name="id" value={id} /> <button type="submit" className="btn btn-xs btn-error">삭제</button> </form> ) } export default DeleteForm;
============> useFormStatus 처
"use client"; import React from 'react' import { deleteTask } from '@/actions/tasks/tasksActions'; import { useFormStatus } from 'react-dom'; interface DeleteFormProps { id:string } const SubmitButton =() =>{ const {pending} = useFormStatus(); return ( <button type="submit" className="btn btn-xs btn-error text-white" disabled={pending} > {pending ? '삭제 중...' : '삭제'} </button> ) } const DeleteForm:React.FC<DeleteFormProps> = ({id}) => { return ( <form action={deleteTask}> <input type='hidden' name="id" value={id} /> <SubmitButton /> </form> ) } export default DeleteForm;
2. 작업 단계 및 설명
1. use client 설정
- Next.js에서 클라이언트 컴포넌트를 정의하기 위해 use client 선언이 필요합니다.
- useFormStatus는 클라이언트에서 동작하는 훅이므로 반드시 클라이언트 컴포넌트에서 사용해야 합니다.
2. useFormStatus로 상태 추적
- useFormStatus 훅은 폼의 상태(예: 제출 중인지 여부)를 확인할 수 있습니다.
- 반환 값 중 pending은 현재 폼이 제출 중인지 나타내는 Boolean 값입니다.
3. 버튼 컴포넌트 (SubmitButton) 분리
- 버튼을 별도의 컴포넌트로 분리한 이유:
- 재사용성을 높이고, 코드 가독성을 개선하기 위함.
- SubmitButton에서 useFormStatus를 활용하여 pending 상태를 기반으로 동작을 처리합니다.
- 기능:
- pending이 true일 경우:
- 버튼 텍스트를 "삭제 중..."으로 변경.
- 버튼 비활성화(disabled).
- pending이 false일 경우:
- 버튼 텍스트는 "삭제".
- 버튼 활성화.
- pending이 true일 경우:
40. Next.js의 Route Handlers 개요 및 활용
앞서 코드 내용은 Server Actions 방식입니다.
다음,
1. Route Handlers란?
- Route Handlers는 Next.js에서 서버리스 함수(Serverless Function)를 구현할 수 있는 강력한 도구입니다.
- Server Actions의 등장으로 사용 빈도가 줄어들었지만, 여전히 특정 상황에서 유용하고 강력한 대안으로 사용됩니다.
2. 기존 방식 vs Route Handlers
기존 방식 (Server Actions 없이)
- 클라이언트에서 폼 데이터를 서버로 전송해야 하는 경우:
- 클라이언트 컴포넌트에서 데이터를 수집합니다.
- 서버로 데이터를 보내기 위해 특정 엔드포인트(API)를 설정합니다.
- 서버에서 데이터를 처리(데이터베이스에 추가, 삭제 등)한 후 응답을 반환합니다.
- 하지만 이 방식은 추가적인 서버 설정이 필요합니다.
- 클라이언트에서 폼 데이터를 서버로 전송해야 하는 경우:
Route Handlers 방식
- Next.js에서 Route Handlers를 통해 빠르게 엔드포인트를 생성할 수 있습니다.
- 별도의 서버를 설정할 필요 없이, 프로젝트 내부에서 엔드포인트를 바로 생성하고 사용할 수 있습니다.
- 예: 데이터베이스에 데이터를 추가하거나 삭제하는 작업을 처리할 수 있음.
3. Route Handlers의 장점
- 빠른 설정:
- 서버를 처음부터 구성할 필요 없이, Next.js 프로젝트 내에서 간단히 엔드포인트를 생성할 수 있습니다.
- 서버리스 환경:
- 서버리스 함수로 동작하며, 요청에 따라 필요한 작업(데이터 추가, 수정, 삭제 등)을 수행합니다.
- 시간 절약:
- 서버 설정 및 관리를 최소화하므로 개발 속도를 높입니다.
4. Route Handlers 활용 시나리오
- 서버 액션(Server Actions)을 사용할 수 없는 경우:
- 예: 클라이언트 컴포넌트에서 직접 데이터베이스 작업을 수행할 수 없는 상황.
- 데이터베이스 작업:
- 클라이언트로부터 받은 데이터를 처리(추가, 삭제 등)하고 결과를 반환.
- 특정 작업 실행:
- 클라이언트에서 서버로 요청을 보내 비즈니스 로직을 실행.
5. Thunder Client 활용
- Thunder Client는 VS Code 확장 프로그램으로, HTTP 요청을 쉽게 테스트할 수 있는 도구입니다.
- 활용 방법:
- 엔드포인트(Route Handlers)를 테스트하기 위해 클라이언트를 설정하지 않아도 됩니다.
- VS Code에서 바로 HTTP 요청을 보내고 응답을 확인할 수 있습니다.
- 설치 방법:
- VS Code에서 확장 프로그램(Extensions) 탭 열기.
- Thunder Client 검색 후 설치.
- 설치 후 바로 요청을 생성하고 테스트 가능.
41.Next.js에서 Route Handlers 설정 및 활용 Get 방식
1. Route Handlers란?
- Next.js에서 API 엔드포인트를 생성하기 위한 강력한 기능.
- 별도의 서버를 설정하지 않고 프로젝트 내에서 서버리스 함수(Serverless Function)를 빠르게 구성할 수 있음.
- 주로 데이터베이스와의 통신, 환경 변수 활용, 외부 API 호출 등의 서버 로직을 처리하기 위해 사용.
2. Route Handlers 구조
폴더와 파일 구성
- app 디렉토리 하위에 api 폴더를 생성 (이 폴더명은 고정).
- api 폴더 내에서 URL 세그먼트를 지정하는 하위 폴더 생성 (예: tasks).
- 하위 폴더 내에 route.js 파일을 생성 (파일명도 고정).
HTTP 메서드별 함수 생성
- GET, POST, PUT, DELETE 등 HTTP 메서드에 대응하는 함수를 작성.
- 함수 이름은 반드시 대문자(GET, POST 등)로 작성해야 해당 요청에 반응.
3. Route Handlers 코드 예제
GET 요청 처리 (tasks 목록 조회)
import { TaskDTO } from "@/dto/TaskDTO"; import prisma from "@/utils/db"; import { NextResponse } from "next/server"; // http://localhost:3000/api/tasks // GET: tasks 조회 export const GET = async () => { try { const tasks = await prisma.task.findMany({ orderBy: { createdAt: "desc", }, }) as TaskDTO[]; return NextResponse.json({ data: tasks }, { status: 200 }); } catch (error) { console.error("GET Error:", error); return NextResponse.json({ error: "Tasks를 불러오는 중 오류가 발생했습니다." }, { status: 500 }); } };
4. 동작 방식
GET 요청
- 브라우저에서 기본적으로 GET 요청을 수행하므로, /api/tasks에 접근하면 자동으로 실행됨.
- 요청 시 데이터베이스에서 tasks 데이터를 조회하여 JSON 형식으로 반환.
경로
- http://localhost:3000/api/tasks 경로에 접근하면 위의 GET 함수가 실행됨.
데이터 처리
- prisma.task.findMany()를 통해 데이터베이스에서 데이터를 조회.
- 정렬 기준(createdAt: "desc")을 설정하여 최신 데이터가 먼저 반환되도록 설정.
5. Route Handlers의 장점
- 빠른 설정: 서버 설정 없이 간단히 엔드포인트 생성 가능.
- 데이터베이스와 통합: 클라이언트에서 직접 데이터베이스와 통신하지 않고 서버를 통해 안전하게 데이터 관리.
- 환경 변수 활용: API 키와 같은 민감한 데이터를 안전하게 처리 가능.
42.Next.js의 Route Handlers에서 HTTP 메서드(GET, POST, PUT, DELETE)에 대응하는 함수를 구현
1. 폴더 및 파일 구조
app/ ├── api/ │ └── tasks/ │ └── route.ts
2. 전체 코드 예제
import { TaskDTO } from "@/dto/TaskDTO"; import prisma from "@/utils/db"; import { NextResponse } from "next/server"; // http://localhost:3000/api/tasks // GET: tasks 조회 export const GET = async () => { try { const tasks = await prisma.task.findMany({ orderBy: { createdAt: "desc", }, }) as TaskDTO[]; return NextResponse.json({ data: tasks }, { status: 200 }); } catch (error) { console.error("GET Error:", error); return NextResponse.json({ error: "Tasks를 불러오는 중 오류가 발생했습니다." }, { status: 500 }); } }; // POST: 새로운 task 추가 export const POST = async (req: Request) => { try { const body = await req.json(); const { title, content } = body; if (!title || !content) { return NextResponse.json( { error: "Title과 Content는 필수 항목입니다." }, { status: 400 } ); } const newTask = await prisma.task.create({ data: { // title, content, }, }); return NextResponse.json({ data: newTask, message: "Task가 생성되었습니다." }, { status: 201 }); } catch (error) { console.error("POST Error:", error); return NextResponse.json({ error: "Task 생성 중 오류가 발생했습니다." }, { status: 500 }); } }; // PUT: 기존 task 수정 export const PUT = async (req: Request) => { try { const body = await req.json(); const { id, title, content } = body; if (!id || !title || !content) { return NextResponse.json( { error: "ID, Title, Content는 필수 항목입니다." }, { status: 400 } ); } const updatedTask = await prisma.task.update({ where: { id }, data: { //title, content }, }); return NextResponse.json({ data: updatedTask, message: "Task가 수정되었습니다." }, { status: 200 }); } catch (error) { console.error("PUT Error:", error); return NextResponse.json({ error: "Task 수정 중 오류가 발생했습니다." }, { status: 500 }); } }; // DELETE: 특정 task 삭제 export const DELETE = async (req: Request) => { try { const body = await req.json(); const { id } = body; if (!id) { return NextResponse.json({ error: "ID는 필수 항목입니다." }, { status: 400 }); } await prisma.task.delete({ where: { id }, }); return NextResponse.json({ message: "Task가 삭제되었습니다." }, { status: 200 }); } catch (error) { console.error("DELETE Error:", error); return NextResponse.json({ error: "Task 삭제 중 오류가 발생했습니다." }, { status: 500 }); } };
POST: 새로운 task 추가
- URL: /api/tasks
- HTTP 메서드: POST
- 작업:
- 요청의 body에서 title과 content를 추출.
- 데이터베이스에 새로운 task 추가.
- 유효성 검사:
- title 또는 content가 없으면 400 Bad Request 응답.
- 응답:
- 성공 시: 생성된 task 데이터 반환(status: 201).
- 실패 시: 오류 메시지 반환(status: 500).
- Next.js의 Route Handlers를 활용하면 HTTP 메서드별 API 엔드포인트를 간단히 구현할 수 있습니다.
- Prisma와 통합하여 데이터베이스 작업을 쉽게 처리할 수 있습니다
43.Next.js 15 미들웨어 설정 방법 정리
1. 미들웨어의 기본 개념
- 미들웨어는 요청이 완료되기 전에 특정 작업을 처리할 수 있는 기능입니다.
- Next.js에서 미들웨어 파일은 반드시 프로젝트 루트에 middleware.ts 또는 middleware.js라는 이름으로 생성해야 합니다.
- 기본적으로 생성된 미들웨어는 모든 경로에서 실행됩니다.
2. 미들웨어 파일 생성
프로젝트 루트에 middleware.ts 파일을 생성합니다.
touch middleware.ts
src 를 디렉토리 경로를 생성한 프로젝트인 경우에는 src 디렉토리 루트에 middleware.ts 파일을 생성합니다.
3. 미들웨어 기본 코드
다음은 middleware.ts 파일에 작성할 기본 코드 예제입니다:
import { NextRequest, NextResponse } from 'next/server'; export function middleware(request: NextRequest) { // 요청 URL 확인 const url = request.nextUrl; console.log(' ***** middleware **** : ',url); // 요청 허용 // return Response.json({ // msg:"Hello there" // }) return NextResponse.redirect(new URL('/', request.url)); //return NextResponse.next(); } export const config = { matcher: ['/about/:path*', '/tasks/:path*'], // 특정 경로에만 미들웨어 적용 };
4. 미들웨어의 주요 기능
(1) 요청 URL 확인
- request.nextUrl을 통해 요청된 URL 정보를 확인할 수 있습니다.
(2) 응답 처리
- NextResponse를 사용하여 요청에 대한 응답을 조작합니다.
- NextResponse.next() : 요청을 그대로 처리.
- NextResponse.redirect() : 특정 URL로 리다이렉트.
(3) 경로 제한
- config.matcher 속성을 사용하여 특정 경로에만 미들웨어를 적용할 수 있습니다.
- :path*를 사용하면 하위 경로까지 포함합니다.
5. 예제 실행 결과
기본 설정
export function middleware(request: NextRequest) { console.log('Middleware 실행!'); return NextResponse.next(); // 요청을 계속 처리 }
- 모든 요청에서 console.log('Middleware 실행!')가 출력됩니다.
특정 경로 리다이렉트
export function middleware(request: NextRequest) { return NextResponse.redirect(new URL('/', request.url)); // 모든 요청을 홈으로 리다이렉트 } export const config = { matcher: ['/about/:path*'], // about 경로만 리다이렉트 };
- /about 및 /about/info 요청 시 홈(/)으로 리다이렉트됩니다.
JSON 응답 반환
export function middleware(request: NextRequest) { return new Response( JSON.stringify({ msg: 'Hello, World!' }), { headers: { 'Content-Type': 'application/json' } } ); } export const config = { matcher: ['/api/:path*'], // /api 경로에만 JSON 응답 };
- /api로 시작하는 요청에 대해 JSON 형태의 응답({ msg: 'Hello, World!' })이 반환됩니다.
6. 코드 실행 흐름
- 미들웨어는 요청이 발생할 때 호출됩니다.
- 요청이 미들웨어 조건에 맞으면, 설정된 동작(리다이렉트, 응답 등)이 실행됩니다.
- NextResponse.next()를 호출하면 요청 처리가 다음 단계로 전달됩니다.
44.Render 백엔드 및 전체 스택 호스팅 서버 이용 및 Netlify 비교
1. Render
주요 기능: 백엔드 및 전체 스택 호스팅
Render는 서버리스 백엔드, 데이터베이스(PostgreSQL), 웹 애플리케이션, 정적 사이트 등을 호스팅하는 플랫폼입니다.특징:
- 백엔드 지원: Node.js, Python, Ruby, Go, Docker 등 다양한 백엔드 환경 지원.
- 데이터베이스: PostgreSQL 데이터베이스를 기본 제공.
- 자동 배포: GitHub/GitLab과 통합하여 코드 변경 시 자동으로 배포.
- 풀스택 가능: 정적 파일뿐 아니라 서버 애플리케이션과 데이터베이스도 호스팅 가능.
- 무료 플랜:
- Hobby 인스턴스는 무료(90일 후 갱신 필요).
- 정적 사이트는 무제한 무료로 제공.
2. Netlify
주요 기능: 정적 웹사이트 및 Jamstack 애플리케이션 호스팅
Netlify는 주로 정적 웹사이트와 Jamstack 아키텍처를 호스팅하는 데 최적화된 플랫폼입니다.특징:
- 정적 웹사이트: React, Vue, Svelte, HTML/CSS/JS 기반 정적 웹사이트에 최적화.
- 서버리스 함수: Netlify Functions를 통해 간단한 서버리스 백엔드 기능 제공(AWS Lambda 기반).
- CI/CD 통합: GitHub/GitLab/Bitbucket과 연동하여 자동 배포 지원.
- 무료 플랜:
- 소규모 프로젝트 및 팀에 적합한 무료 플랜 제공.
- 무료 SSL 인증서 제공.
3. 차이점
4. 결론
- Render는 백엔드와 데이터베이스가 필요한 풀스택 애플리케이션에 적합한 플랫폼입니다.
- Netlify는 정적 사이트 또는 Jamstack 구조의 프로젝트에 더 적합합니다.
둘 중 선택은 프로젝트의 요구사항에 따라 달라지며, 풀스택 기능이 필요하면 Render를, 정적 사이트나 Jamstack을 원한다면 Netlify를 사용하는 것이 좋습니다.
45.Vercel 배포 전 로컬에서 Next.js와 Prisma 프로젝트 준비 및 테스트
1. 빌드 명령어 수정
- Vercel에 배포하기 전에 npx prisma generate 명령어를 추가하여 Prisma 스키마를 생성하고, 그 후 next build를 실행해야 합니다.
- package.json 수정:
"build": "npx prisma generate && next build"
2. Prisma 예제 수정
- 기존 코드: 데이터베이스에 항목을 생성하는 로직 포함.
- 변경 사항:
- 항목 생성 코드를 주석 처리(참고용으로 남겨둠).
- 로그를 추가하여 Prisma 관련 동작 확인 가능하도록 설정.
- 데이터가 없을 경우 "No tasks"와 같은 안내 메시지 표시.
3. 데이터베이스 초기화
- Prisma Studio 또는 tasks 페이지에서 데이터베이스의 모든 항목을 삭제.
- 초기 상태로 시작하도록 데이터베이스를 깨끗하게 정리.
4. 로컬 빌드
개발 서버를 중단한 뒤, 다음 명령어로 프로젝트를 빌드:
npm run build
빌드 완료 후, npm start로 로컬 서버 실행
Route (app) Size First Load JS ┌ ○ / 184 B 109 kB ├ ○ /_not-found 979 B 106 kB ├ ƒ /about/[id] 184 B 109 kB ├ ○ /about/info 184 B 109 kB ├ ƒ /api/tasks 146 B 106 kB ├ ƒ /auth/[[...sign-in]] 146 B 106 kB ├ ○ /client 388 B 106 kB ├ ○ /drinks 186 B 114 kB ├ ƒ /drinks/[drinksId] 186 B 114 kB ├ ○ /prisma-example 146 B 106 kB ├ ○ /query 184 B 109 kB ├ ○ /tasks 1.28 kB 115 kB └ ƒ /tasks/[taskId] 184 B 109 kB + First Load JS shared by all 105 kB ├ chunks/4bd1b696-fab049316e6b4e07.js 52.9 kB ├ chunks/517-bee589a8bee21748.js 50.5 kB └ other shared chunks (total) 1.96 kB ƒ Middleware 31.9 kB
5. Next.js의 정적 및 동적 렌더링 이해
- Next.js 빌드 과정에서 생성된 페이지의 상태를 확인.
- 빌드 결과는 두 가지 형태로 나뉨:
- 정적 HTML (○ 표시):
- 빌드 시 미리 생성되어 서버 요청 없이도 빠르게 로드 가능.
- 예: /drinks
- 서버 렌더링 (ƒ 표시):
- 사용자가 페이지를 요청할 때마다 서버에서 데이터를 가져와 렌더링.
- 예: /drinks/[drinksId]
- 정적 HTML (○ 표시):
페이지 렌더링 차이점:
- 정적 페이지: /drinks는 빌드 과정에서 생성된 HTML이므로 로딩 스피너 없이 즉시 표시.
- 동적 페이지: /drinks/[drinksId]는 특정 ID에 대한 데이터를 서버에서 가져오므로 처음 방문 시 로딩 스피너가 표시됨.
- 캐싱을 활용하여 동일한 페이지 재방문 시 빠르게 로드.
6. Prisma 예제 동작 확인
- Prisma 예제 페이지(/prisma-example)는 정적 HTML로 생성됨.
- 이 페이지는 빌드 시 한 번 생성되므로 데이터베이스 변경사항이 즉시 반영되지 않음.
7. Task 기능 테스트
- /tasks 페이지에서는 동적 렌더링을 통해 데이터가 실시간으로 반영됨.
- 주요 테스트:
- 추가: 새로운 작업 추가 후 즉시 렌더링 확인.
- 수정: 작업 내용 수정 및 완료 상태 변경.
- 삭제: 작업 삭제 후 데이터가 정상적으로 제거되는지 확인.
- Prisma Studio에서 데이터 변경사항을 확인하여 동작 검증.
46.Next.js에서 정적 페이지와 동적 페이지의 동기화 문제 해결 및 배포 준비
문제 상황
- /tasks 페이지가 기본적으로 정적으로 생성(static)되며, 데이터 변경 시 최신 상태를 반영하지 못하는 경우가 발생.
- 예를 들어:
- 첫 번째 작업 추가 → 정상 표시.
- 두 번째 작업 추가 후 다시 /tasks로 돌아가면 첫 번째 작업만 보임.
- 삭제된 작업이 다시 표시되거나, 데이터베이스에 없는 작업을 삭제하려고 하면 에러 발생.
원인 분석
Next.js는 **정적 페이지(Static)**와 **동적 페이지(Dynamic)**를 구분하여 최적화.
- 기본적으로 getStaticProps를 사용해 정적 페이지를 빌드 타임에 생성.
- /tasks 페이지가 정적으로 생성되었기 때문에 데이터 변경 사항이 즉시 반영되지 않음.
/tasks/[taskId]는 동적 페이지로 설정되어 잘 동작하지만, /tasks는 정적으로 렌더링되면서 데이터 갱신이 지연됨.
해결 방법: dynamic 변수 사용
Next.js에서 페이지를 강제로 동적으로 렌더링하도록 설정.
수정 사항:
/tasks 페이지에서 dynamic 변수 추가:
export const dynamic = 'force-dynamic';
src/app/tasks/page.tsx
//import TaskForm from '@/components/tasks/TaskForm'; import TaskFormCustom from '@/components/tasks/TaskFormCustom'; import TaskList from '@/components/tasks/TaskList'; import React from 'react' export const dynamic = 'force-dynamic'; const TasksPage:React.FC = () => { return ( <div> <h1 className="text-2xl mb-10">작업 페이지</h1> <TaskFormCustom /> <TaskList /> </div> ); } export default TasksPage;
설정을 통해 /tasks 페이지가 항상 서버에서 동적으로 렌더링되도록 강제함.
로딩 페이지 추가
동적 페이지로 변경하면, 데이터 로드 중 로딩 스피너를 표시할 수 있음. 이를 위해 loading.tsx 파일을 추가.
app/tasks/loading.tsx:
const loading = () => { return ( <div className="w-full h-screen flex justify-center items-center "> <span className="w-28 loading"> Loading... </span> </div> ); }; export default loading;
테스트 과정
동적 렌더링 확인:
- npm run build → npm start로 실행.
- 빌드 결과에서 /tasks가 정적(○)에서 동적(ƒ)으로 변경된 것을 확인.
동작 테스트:
- 작업 추가, 삭제 후 /tasks를 새로고침해도 데이터가 정상적으로 갱신됨.
- 데이터베이스와 동기화된 상태 유지.
로딩 스피너 확인:
- /tasks에 접속 시 데이터를 가져오는 동안 로딩 스피너가 표시됨.
Route (app) Size First Load JS ┌ ○ / 184 B 109 kB ├ ○ /_not-found 979 B 106 kB ├ ƒ /about/[id] 184 B 109 kB ├ ○ /about/info 184 B 109 kB ├ ƒ /api/tasks 149 B 106 kB ├ ƒ /auth/[[...sign-in]] 149 B 106 kB ├ ○ /client 388 B 106 kB ├ ○ /drinks 186 B 114 kB ├ ƒ /drinks/[drinksId] 186 B 114 kB ├ ○ /prisma-example 149 B 106 kB ├ ○ /query 184 B 109 kB ├ ƒ /tasks 1.28 kB 115 kB └ ƒ /tasks/[taskId] 184 B 109 kB + First Load JS shared by all 105 kB ├ chunks/4bd1b696-fab049316e6b4e07.js 52.9 kB ├ chunks/517-bee589a8bee21748.js 50.5 kB └ other shared chunks (total) 1.96 kB ƒ Middleware 31.9 kB
요약
문제 해결:
- /tasks 페이지에 dynamic = 'force-dynamic' 설정 추가로 서버에서 동적 렌더링 강제.
- 정적 HTML이 아닌 최신 데이터 기반으로 페이지가 렌더링.
추가 작업:
- 로딩 페이지 추가(loading.tsx).
- 작업 추가, 삭제 테스트 후 동작 확인.
배포 준비 완료:
- 정적 페이지와 동적 페이지의 문제를 해결한 상태로 Vercel에 배포 가능.
댓글 ( 0)
댓글 남기기