juni
Next.js Rendering , Route Handlers 본문
클라이언트 컴포넌트와 서버 컴포넌트
라우팅에 상관 없는 기타 버튼 , 네비게이션 등과 같은 경우는 app폴더 바깥의 components 폴더로 관리
- 서버 컴포넌트
// src>app>page.tsx
export default function Home() {
console.log("여기는 어디일까요?");
return (
<div className="p-8">
안녕하세요! 내배캠 리액트.. 아니아니 넥스트입니다!
</div>
);
}
alert는 서버 컴포넌트에서 사용 불가
os.hostname은 서버 컴포넌트에서(Node.js 런타임 환경) 사용이 가능
// src>app>page.tsx
export default function Home() {
// console.log("여기는 어디일까요?");
const os = require("os");
console.log("Hostname:", os.hostname());
return (
<div className="p-8">
안녕하세요! 내배캠 리액트.. 아니아니 넥스트입니다!
</div>
);
}
- 클라이언트 컴포넌트
- “use client”; 코드를 컴포넌트 최상단에 붙여줘야함
아래와 같은 오류 시 클라이언트 컴포넌트로 바꿔줘야함
언제, 어떻게 SC/CC를 써야 하나요?
대체로 User와의 상호작용이 있는 경우 CC를, 그 외의 경우는 SC를 쓰도록 권장 -> 일단 SC로 사용하다 오류 발생 시 CC로 바꾸는게 실용적임
예시코드
// src>app>page.tsx
import Button from "@/components/Button";
// src>app>page.tsx
export default function Home() {
return (
<div className="p-8">
안녕하세요! 내배캠 리액트.. 아니아니 넥스트입니다!
<section>
<h1>제목</h1>
<p>내용</p>
<ul>
<li>항목1</li>
<li>항목2</li>
<li>항목3</li>
</ul>
</section>
<Button />
</div>
);
}
// src>components>Button.tsx
"use client";
import React from "react";
const Button = () => {
return (
<button
onClick={() => {
alert("안녕하세요!");
}}
>
클릭
</button>
);
};
export default Button;
import Counter from "@/components/Counter";
// src>app>page.tsx
export default function Home() {
return (
<div className="p-8">
안녕하세요! 내배캠 리액트.. 아니아니 넥스트입니다!
<Counter />
</div>
);
}
"use client";
import React from "react";
import { useState } from "react";
const Counter = () => {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
};
return (
<div>
<div>{count}</div>
<button onClick={handleClick}>클릭</button>
</div>
);
};
export default Counter;
렌더링 이해를 위한 핵심 개념
- SSG(Static Site Generation)
- fetch한 데이터가 영원히 변하지 않음. 계속 컴포넌트 갱신 할 필요가 없음
- SSG는 빌드타임(build-time) 때만 컴포넌트를 생성하고, 이후는 변하지 않는 페이지로 가정하여 static 컴포넌트를 제공하는 것 그리고 Next.js는 아-무것도 하지 않으면 기본적으로 SSG로 동작
- 예시코드 : fetch에 옵션 주거나 아무 옵션도 주지 않기
import Image from "next/image";import React from "react";
import type { RandomUser } from "@/types";
// SSG TEST : 아무것도 하지 않으면 SSG!const SSG = async () => {// (1) 첫 번째 방법 : 아무 옵션도 부여 x// (2) 두 번째 방법 : fetch에 force-cache 옵션 주기cache: "force-cache",});const { results } = await response.json();const user: RandomUser = results[0];
return (<div className="mt-8"><div className="border p-4 my-4"><div className="flex gap-8">{/* 유저 기본정보 */}<div><Imagesrc={user.picture.large}alt={user.name.first}width={200}height={200}/><h2 className="text-xl font-bold">{user.name.title}. {user.name.first} {user.name.last}</h2><p className="text-gray-600">{user.email}</p>
<div className="mt-4"><p><span className="font-bold">전화번호 : </span>{user.phone}</p><p><span className="font-bold">휴대전화번호 : </span>{user.cell}</p><p><span className="font-bold">사는 곳 : </span>{user.location.city}, {user.location.country}</p><p><span className="font-bold">등록일자 : </span>{new Date(user.registered.date).toLocaleDateString()}</p>
<p><span className="font-bold">생년월일 : </span>{new Date(user.dob.date).toLocaleDateString()}</p></div></div>
{/* 지도영역 */}<iframesrc={`https://maps.google.com/maps?q=${user.location.coordinates.longitude},${user.location.coordinates.latitude}&z=15&output=embed`}height="450"width="600"></iframe></div></div></div>);};
export default SSG;
- ISR(Incremental Site Regeneration)
- fetch한 데이터는 가끔 변함. 일정 주기마다 가끔식만 컴포넌트 갱신
- ISR은 빌드타임(build-time) 때 컴포넌트를 초기 생성하고, 이후는 일정 주기마다 변화를 적용하여 컴포넌트를 제공하는 것
- 예시코드
- 첫 번째 방법 : fetch에 옵션 주기
import Image from "next/image";import React from "react";
import type { RandomUser } from "@/types";
const ISR = async () => {next: {// 5초마다 새로운 데이터 가져옴revalidate: 5,},});const { results } = await response.json();const user: RandomUser = results[0];
return (<div className="mt-8"><div className="border p-4 my-4"><div className="flex gap-8">{/* 유저 기본정보 */}<div><Imagesrc={user.picture.large}alt={user.name.first}width={200}height={200}/><h2 className="text-xl font-bold">{user.name.title}. {user.name.first} {user.name.last}</h2><p className="text-gray-600">{user.email}</p>
<div className="mt-4"><p><span className="font-bold">전화번호 : </span>{user.phone}</p><p><span className="font-bold">휴대전화번호 : </span>{user.cell}</p><p><span className="font-bold">사는 곳 : </span>{user.location.city}, {user.location.country}</p><p><span className="font-bold">등록일자 : </span>{new Date(user.registered.date).toLocaleDateString()}</p>
<p><span className="font-bold">생년월일 : </span>{new Date(user.dob.date).toLocaleDateString()}</p></div></div>
{/* 지도영역 */}<iframesrc={`https://maps.google.com/maps?q=${user.location.coordinates.longitude},${user.location.coordinates.latitude}&z=15&output=embed`}height="450"width="600"></iframe></div></div></div>);};
export default ISR; - 두번째 방법 : page.tsx 컴포넌트에 revalidate 추가하기 ( ISR.tsx에 있는 next 옵션은 지워야함 ) -> 페이지 단위로 주기적으로 새로운 데이터로 갱신 가능
// src>app>rendering>page.tsx
import ISR from "@/components/rendering/ISR";import React from "react";
export const revalidate = 5;
const RenderingTestPage = () => {return (<div><h1>4가지 렌더링 방식을 테스트합니다.</h1><ISR /></div>);};
export default RenderingTestPage;
- 첫 번째 방법 : fetch에 옵션 주기
- SSR(Server Side Rendering)
- fetch한 데이터는 실시간으로 계속 바뀜. 컴포넌트 요청이 있을 때 마다 데이터를 갱신해서 최신 데이터만 제공 필요
- SSR은 빌드타임(build-time) 때 컴포넌트를 초기 생성하고, 이후 컴포넌트 요청이 있을 때 마다 변화를 적용하여 가장 최신의 데이터를 user에게 제공
- 예시 코드 : fetch에 옵션 주기 -> hydration이 완료되기 전까지의 시간. 즉, TTI(Time To Interactive)가 관건
// src>components>rendering>SSR.tsximport Image from "next/image";import React from "react";
import type { RandomUser } from "@/types";
const SSR = async () => {cache: "no-cache",});const { results } = await response.json();const user: RandomUser = results[0];
return (<div className="mt-8"><div className="border p-4 my-4"><div className="flex gap-8">{/* 유저 기본정보 */}<div><Imagesrc={user.picture.large}alt={user.name.first}width={200}height={200}/><h2 className="text-xl font-bold">{user.name.title}. {user.name.first} {user.name.last}</h2><p className="text-gray-600">{user.email}</p>
<div className="mt-4"><p><span className="font-bold">전화번호 : </span>{user.phone}</p><p><span className="font-bold">휴대전화번호 : </span>{user.cell}</p><p><span className="font-bold">사는 곳 : </span>{user.location.city}, {user.location.country}</p><p><span className="font-bold">등록일자 : </span>{new Date(user.registered.date).toLocaleDateString()}</p>
<p><span className="font-bold">생년월일 : </span>{new Date(user.dob.date).toLocaleDateString()}</p></div></div>
{/* 지도영역 */}<iframesrc={`https://maps.google.com/maps?q=${user.location.coordinates.longitude},${user.location.coordinates.latitude}&z=15&output=embed`}height="450"width="600"></iframe></div></div></div>);};
export default SSR;
- CSR(Client Side Rendering)
- fetch한 데이터는 실시간으로 계속 바뀜. 컴포넌트 요청이 있을 때 마다 데이터를 갱신해서 최신 데이터만 제공 필요
- SR은 빌드타임에 컴포넌트를 초기 생성하진 않음. Javascript로 이루어진 리액트 파일을 다운로드 받고 그제서야 화면이 그려지게 됨
- 예시코드 : "use client" 최상단에 추가
"use client";
import Image from "next/image";import React, { useEffect, useState } from "react";
import type { RandomUser } from "@/types";
const CSR = () => {// 상태 관리const [user, setUser] = useState<RandomUser | null>(null);
useEffect(() => {const fetchUser = async () => {const { results } = await response.json();setUser(results[0]);};
fetchUser();}, []);// SSR과의 차이 -> 모든 데이터를 불러온 뒤 불러오면 느릴 수 있으므로 대처if (!user) {return <div>로딩중...</div>;}
return (<div className="mt-8"><div className="border p-4 my-4"><div className="flex gap-8">{/* 유저 기본정보 */}<div><Imagesrc={user.picture.large}alt={user.name.first}width={200}height={200}/><h2 className="text-xl font-bold">{user.name.title}. {user.name.first} {user.name.last}</h2><p className="text-gray-600">{user.email}</p>
<div className="mt-4"><p><span className="font-bold">전화번호 : </span>{user.phone}</p><p><span className="font-bold">휴대전화번호 : </span>{user.cell}</p><p><span className="font-bold">사는 곳 : </span>{user.location.city}, {user.location.country}</p><p><span className="font-bold">등록일자 : </span>{new Date(user.registered.date).toLocaleDateString()}</p>
<p><span className="font-bold">생년월일 : </span>{new Date(user.dob.date).toLocaleDateString()}</p></div></div>
{/* 지도영역 */}<iframesrc={`https://maps.google.com/maps?q=${user.location.coordinates.longitude},${user.location.coordinates.latitude}&z=15&output=embed`}height="450"width="600"></iframe></div></div></div>);};
export default CSR;
Full Stack
백엔드 로직을 신경쓰지 않고, 우리는 json-server , supabase , firebase 등에 요청만 하는 것 -> BaaS 이용
FE 와 BE를 합친게 Next.js
Router Handlers
app directory 내부에 route.ts 파일을 만나면 기본적으로 Next.js는 router handlers로 인식 새로운 폴더
- api>apiTest를 만든 후, 하위에 route.ts 파일 생성 , 메소드만 요청하면 URL은 알아서 인식 함
export async function GET(request: Request) {console.log("GET /api/test");}
export async function POST(request: Request) {console.log("POST /api/test");}
export async function PUT(request: Request) {console.log("PUT /api/test");}
export async function DELETE(request: Request) {console.log("DELETE /api/test");}
export async function PATCH(request: Request) {console.log("PATCH /api/test");} - Thunder Client 등 API testing 도구 활용하여 테스트
실습 세팅
- 환경세팅
- src>app>api>practice>route.ts 파일을 생성
- json-server를 설치
- db.json을 생성하여 todos를 생성
- 다음 명령어를 통해 DB 서버를 시작* 사실 DB 서버가 아니라 BaaS로 이해해야 더 맞지만, 실습 편의상 DB서버라 표현.
- yarn json-server db.json --port 4000
- GET POST 코드
-
// src>app>api>practice>route.tsexport async function GET(request: Request) {const response = await fetch(`http://localhost:4000/todos`);const todos = await response.json();
if (!todos) {return new Response("todos not found", { status: 404 });}
return Response.json({ todos });}
export async function POST(request: Request) {// body에서 값을 뽑아오기const { title, contents } = await request.json();
const response = await fetch(`http://localhost:4000/todos`, {method: "POST",headers: {"Content-Type": "application/json",},// id 값은 자동 생성body: JSON.stringify({ title, contents, isDone: false }),});const todo = await response.json();
return Response.json({ todo });}
-
Next.js 에서의 React Query ( tanstack query ) 세팅
- 패키지 설치 : yarn add @tanstack/react-query
- query provider를 담는 생성
// src>app>provider.tsx"use client";
import React from "react";import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
function QueryProvider({ children }: React.PropsWithChildren) {// app 내에서 1번만 해야 하나의 QueryClient내부에 저장됨 , mutation 시 사용const queryClient = new QueryClient();
return (<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>);}
export default QueryProvider; -
/// src>app>layout.tsx
import type { Metadata } from "next";import { Inter } from "next/font/google";import "./globals.css";import Link from "next/link";import QueryProvider from "./provider";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {title: "Sparta Next App",description: "This is awesome Website",};
export default function RootLayout({children,}: Readonly<{children: React.ReactNode;}>) {return (<html lang="en"><body className={inter.className}><nav><Link href="/">Home</Link><Link href="/about">About</Link><Link href="/contact">Contact</Link><Link href="/blog">Blog</Link></nav><QueryProvider> {children} </QueryProvider></body></html>);} - Next로 TodoList 만들기
- page.tsx 생성하여 조회기능 작성
// src>app>todos>page.tsx"use client";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";import React, { useState } from "react";
import type { NewTodo, Todo } from "@/types";
const TodosPage = () => {const queryClient = useQueryClient();
const [title, setTitle] = useState("");const [contents, setContents] = useState("");
const {data: todos,isLoading,isError,} = useQuery({queryKey: ["todos"],queryFn: async () => {const response = await fetch(`http://localhost:4000/todos`);const todos = await response.json();return todos;},});
const newTodoMutation = useMutation({mutationFn: async (newTodo: NewTodo) => {const response = await fetch(`http://localhost:4000/todos`, {method: "POST",headers: {"Content-Type": "application/json",},body: JSON.stringify(newTodo),});const todo = await response.json();return todo;},});
if (isLoading) {return <div>Loading...</div>;}
if (isError) {return <div>Error</div>;}
return (<div><h1>투두리스트입니다.</h1><p>Here you can see all your todos</p>
<section><h2>새로운 투두 추가하기</h2><formonSubmit={(e) => {e.preventDefault();newTodoMutation.mutate({ title, contents },{onSuccess: () => {setTitle("");setContents("");
queryClient.invalidateQueries({queryKey: ["todos"],});},});}}><div><label htmlFor="title">Title</label><inputid="title"type="text"value={title}onChange={(e) => setTitle(e.target.value)}/></div><div><label htmlFor="contents">Contents</label><inputid="contents"type="text"value={contents}onChange={(e) => setContents(e.target.value)}/></div><button type="submit">Add Todo</button></form></section>
{todos.map((todo: Todo) => {return (<divkey={todo.id}className="bg-blue-100 border border-blue-400 text-blue-700 p-8 m-2 rounded"><h2>{todo.title}</h2><p>{todo.contents}</p>{todo.isDone ? <p>Done</p> : <p>Not done</p>}</div>);})}</div>);};
export default TodosPage;
- page.tsx 생성하여 조회기능 작성
'CS' 카테고리의 다른 글
flex-shrink, object-cover (0) | 2024.08.18 |
---|---|
Next.js 6가지 원칙 및 4가지 랜더링 기법 (0) | 2024.07.19 |
Next.js 라우팅 (0) | 2024.07.02 |
Next.js (0) | 2024.07.01 |
Study_CSS 및 로직 예시 (0) | 2024.07.01 |