본문 바로가기

카테고리 없음

Study_240703 ( 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
        const response = await fetch(`https://randomuser.me/api`);
        // (2) 두 번째 방법 : fetch에 force-cache 옵션 주기
          const response = await fetch(`https://randomuser.me/api`, {
            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>
                  <Image
                    src={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>

                {/* 지도영역 */}
                <iframe
                  src={`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 () => {
          const response = await fetch(`https://randomuser.me/api`, {
            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>
                    <Image
                      src={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>

                  {/* 지도영역 */}
                  <iframe
                    src={`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;
  • SSR(Server Side Rendering)
    • fetch한 데이터는 실시간으로 계속 바뀜. 컴포넌트 요청이 있을 때 마다 데이터를 갱신해서 최신 데이터만 제공 필요
    • SSR은 빌드타임(build-time) 때 컴포넌트를 초기 생성하고, 이후 컴포넌트 요청이 있을 때 마다 변화를 적용하여 가장 최신의 데이터를 user에게 제공
    • 예시 코드 : fetch에 옵션 주기 -> hydration이 완료되기 전까지의 시간. 즉, TTI(Time To Interactive)가 관건  
      // src>components>rendering>SSR.tsx
      import Image from "next/image";
      import React from "react";

      import type { RandomUser } from "@/types";

      const SSR = async () => {
        const response = await fetch(`https://randomuser.me/api`, {
          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>
                  <Image
                    src={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>

                {/* 지도영역 */}
                <iframe
                  src={`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 response = await fetch(`https://randomuser.me/api`);
            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>
                  <Image
                    src={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>

                {/* 지도영역 */}
                <iframe
                  src={`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 도구 활용하여 테스트

 

실습 세팅

  • 환경세팅
    1. src>app>api>practice>route.ts 파일을 생성
    2. json-server를 설치
    3. db.json을 생성하여 todos를 생성
    4. 다음 명령어를 통해 DB 서버를 시작* 사실 DB 서버가 아니라 BaaS로 이해해야 더 맞지만, 실습 편의상 DB서버라 표현.
    5. yarn json-server db.json --port 4000
  • GET POST 코드
    • // src>app>api>practice>route.ts
      export 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>
              <form
                onSubmit={(e) => {
                  e.preventDefault();
                  newTodoMutation.mutate(
                    { title, contents },
                    {
                      onSuccess: () => {
                        setTitle("");
                        setContents("");

                        queryClient.invalidateQueries({
                          queryKey: ["todos"],
                        });
                      },
                    }
                  );
                }}
              >
                <div>
                  <label htmlFor="title">Title</label>
                  <input
                    id="title"
                    type="text"
                    value={title}
                    onChange={(e) => setTitle(e.target.value)}
                  />
                </div>
                <div>
                  <label htmlFor="contents">Contents</label>
                  <input
                    id="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 (
                <div
                  key={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;