본문 바로가기

Frontend Study

[유데미x스나이퍼팩토리] Next.js 3기 - 사전직무교육 7일차

스타일 적용 방법

  1. CSS 파일 임포트
  2. 모듈
  3. 테일윈드
  4. CSS in JS

폰트

  1. next/font/google
  2. next/font/local
  3. @font-face

라우팅

앱 라우팅

  • 라우팅은 app 폴더 아래에 폴더를 생성하면, 해당 폴더의 이름이 경로로 지정됨
  • 폴더 안에는 page 컴포넌트를 넣어 페이지를 렌더링
  • app 폴더 아래에는 라우팅을 담당하는 컴포넌트 및 폴더들만 넣어야 함
  • app 폴더 바로 아래의 page가 메인 페이지 담당

Next.js는 다양한 시스템 파일을 이용해서 라우팅 및 에러를 제어한다

page

라우팅 결과를 보여주는 페이지

layout

html 구조를 잡아줌

→ 하나의 레이아웃을 여러 페이지에서 사용 가능

not fount

not-found.tsx 파일을 통해 잘못된 라우팅 경로 핸들링

최상단에 작성되어야 함

loading

loading.tsx 파일을 통해 에러 핸들링

error

에러 발생시 나타나는 페이지

에러가 발생한 컴포넌트와 가장 가까운 error.tsx가 실행됨

다이나믹 라우팅

폴더명(경로) 뒤에 [id]를 붙여주면 id에 따라 동적으로 변동되는 페이지 생성

데이터 통신

기존의 fetch.api는 웹 브라우저에서 제공하는 클라이언트 api

→ SSR은 서버에서 실행되므로, 클라이언트 api와는 잘 맞지 않음

→ next.js 팀에서 fetch.api를 개선

  1. 클라이언트
"use client";

import { useEffect, useState } from "react";

function ClientFetch() {
  const [data, setData] = useState(null);
  const fetchData = async () => {
    const res = await fetch("<https://jsonplaceholder.typicode.com/todos>");
    const data = await res.json();
    setData(data);
  };
  useEffect(() => {
    fetchData();
  }, []);
  return (
    <>

클라이언트 페치

{JSON.stringify(data)}
    
  );
}
export default ClientFetch;

  • 클라이언트 렌더링 과정에서 서버 통신이 발생 → 지연 발생
  • useState, useEffect 등 hook을 사용 가능
  1. 서버 컴포넌트
async function ServerFetch() {
  const res = await fetch("<https://jsonplaceholder.typicode.com/todos>");
  const data = await res.json();
  return (
    <>

서버 페치

{JSON.stringify(data)}
    
  );
}
export default ServerFetch;

  • 서버에서 이미 데이터를 받아와서 사용하기 때문에 지연이 없음
  • React Hook을 사용 할 필요 없이 바로 변수에 할당 가능

로딩 및 에러

클라이언트에서는 직접 로딩 및 에러 처리를 해줘야 함

"use client";

import { useEffect, useState } from "react";

function ClientFetch() {
  const [data, setData] = useState(null);
  const [isLoading, setIsLoading] = useState(false);
  const [isError, setIsError] = useState(false);
  const fetchData = async () => {
    setIsLoading(true);
    await new Promise((res) => setTimeout(res, 3000));
    const res = await fetch("<https://jsonplaceholder.typiasdascode.com/todos>");
    if (!res.ok) setIsError(true);
    const data = await res.json();
    setData(data);
    setIsLoading(false);
  };
  useEffect(() => {
    fetchData();
  }, []);

  if (isLoading) return

로딩중입니다....

;
  if (isError) return

에러 발생!!

;
  return (
    <>

클라이언트 페치

{JSON.stringify(data)}
    
  );
}
export default ClientFetch;

서버 컴포넌트에서는 라우터 폴더 아래 loading.tsx 컴포넌트를 만들어 로딩 처리

// loading.tsx
function loading() {
  return <div>서버 로딩중...</div>;
}
export default loading;

// error.tsx
"use client";

function error() {
  return <div>에러 발생!</div>;
}
export default error;

멀티 스레드

promise.all()을 사용하면 api 호출을 병렬로 처리 가능

const getPosts = async () => {
  try {
    await new Promise((res) => setTimeout(res, 3000));
    const res = await fetch("<https://jsonplaceholder.typicode.com/todos>");
    if (!res.ok) throw new Error("에러 발생");
    console.log(res);
    return await res.json();
  } catch {
    throw new Error("에러 발생");
  }
};
const getTodos = async () => {
  try {
    await new Promise((res) => setTimeout(res, 3000));
    const res = await fetch("<https://jsonplaceholder.typicode.com/todos>");
    if (!res.ok) throw new Error("에러 발생");
    return await res.json();
  } catch {
    throw new Error("에러 발생");
  }
};

async function ServerFetch() {
  const [data1, data2] = await Promise.all([getPosts(), getTodos()]);
  return (
    <>

서버 페치

{JSON.stringify(data1)}

서버 페치2

{JSON.stringify(data2)}
    
  );
}
export default ServerFetch;

시간이 걸리는(로딩) 컴포넌트들은 Suspense 컴포넌트로 감싸서 병렬 처리 가능

→ fallback props 안에 로딩 컴포넌트를 넣어줌

import Post from "@/app/server-fetch/post";
import Todo from "@/app/server-fetch/todo";
import { Suspense } from "react";

async function ServerFetch() {
  return (
    <>
      Post 로딩중}>
        
      
      Todo 로딩중}>
        
      
    
  );
}
export default ServerFetch;

// Post.tsx
const getPosts = async () => {
  try {
    await new Promise((res) => setTimeout(res, 3000));
    const res = await fetch("<https://jsonplaceholder.typicode.com/todos/1>");
    if (!res.ok) throw new Error("에러 발생");
    return await res.json();
  } catch {
    throw new Error("에러 발생");
  }
};

async function Post() {
  const data = await getPosts();
  return (

Post

{JSON.stringify(data, null, 2)}
); } export default Post; // Todo.tsx const getTodos = async () => { try { // await new Promise((res) => setTimeout(res, 5000)); const res = await fetch("<https://jsonplaceholder.typicode.com/todos/1>"); if (!res.ok) throw new Error("에러 발생"); return await res.json(); } catch { throw new Error("에러 발생"); } }; async function Todo() { const data = await getTodos(); return (

Todo

{JSON.stringify(data, null, 2)}
); } export default Todo;

캐싱

Next.js는 데이터를 받아오거나 연산을 했을 때 ‘캐싱’을 실행

→ 데이터를 다시 받아올 때 캐싱 된 데이터를 활용함으로써 빠르게 받아 올 수 있음

캐싱 매커니즘

  1. fetch 요청이 발생할 경우, 라우트 캐시에서 먼저 캐싱 된 데이터를 찾음
    1. 캐싱 된 데이터가 있을 경우 (HIT) 해당 데이터 사용
    2. 캐싱 된 데이터가 없을 경우 (MISS), 데이터를 캐싱 후 (SET) 풀 라우트 캐시 또는 리퀘스트 메모이제이션에 요청을 전달
    3. 같은 요청이 들어오면 SET된 데이터를 사용
    새로고침 또는 revalidate 등을 통해 캐싱 삭제 가능
  2. 풀 라우트 캐시에서 데이터를 찾음 (빌드시에만)
    1. 캐싱 된 데이터가 있을 경우 (HIT) 해당 데이터 사용
    2. 캐싱 된 데이터가 없을 경우 (MISS), 데이터를 캐싱한 뒤 리퀘스트 메모이제이션에 요청을 전달
    → Re-Build 또는 revalidate를 통해 삭제 가능
    • 빌드 과정에서만 사용 → 지속시간이 영구적 → 라우트 캐시가 초기화 되어도 풀 라우트 캐시가 삭제되지 않는다면 계속해서 캐싱된 데이터를 받게 됨
    • 정적인 페이지는 Full-Route-Cache의 적용 대상이 됨
    • 반대로 동적인 페이지는 적용 안됨 → 라우트 캐시만 사용
    • 상황에 따라 정적/동적 페이지를 적절히 활용하여 컴포넌트 구성

👏 페이지 동적으로 변경하는 방법

→export const revalidate = 0

  1. 리퀘스트 메모이제이션⇒ 여러 컴포넌트에서 fetch 함수를 호출한다 하더라도 내부적으로는 메모이제이션을 통해 한번만 요청을 함으로써 동일한 데이터를 가져올 수 있음
    1. 캐싱 된 데이터가 있을 경우 (HIT) 해당 데이터 사용
    2. 캐싱 된 데이터가 없을 경우 (MISS), 데이터 캐시에 데이터 요청
    쿼리 파라미터가 바뀌면 새로운 url로 인식
  2. → 동일한 URL과 옵션을 가진 요청을 자동으로 메모이제이션하여 fetch API 를 확장
  3. 데이터 개시
    1. 캐싱 된 데이터가 있을 경우 (HIT) 해당 데이터 사용
    2. 캐싱 된 데이터가 없을 경우 (MISS), 비로소 외부 데이터 소스로부터 데이터를 받아 옴
    데이터 캐시는 영구적으로 지속됨 → 직접 삭제 해야 함

각 단계에 캐싱된 데이터가 있으면, 후속 단계는 아예 실행 되지 않음

캐시 무효화

캐시는 데이터를 저장해 둠으로써 화면을 빠르게 렌더링 할 수 있다는 장점이 있지만, 반대로 동적인 페이지에서는 데이터가 업데이트 되지 않는 오류가 발생 할 수 있음

  1. 시간 기반 재검증
    • 정해져 있는 시간동안 저장된 캐시를 사용
    • 지정한 시간이 지나도 자동으로 캐시가 비워지고 새로운 캐시가 저장되진 않음
    • export const revalidate = 0 ⇒ 캐시를 사용하지 않겠다는 의미
next: { revalidate: 60 }
// 60초동안만 캐시를 사용
  1. 명령어 기반 재검증
    • 명령어를 입력하여 캐시를 삭제
    • cache: no-store
  2. 주문형 재검증
    • revalidatePath("/")
    • 바로 데이터를 세팅하는 것이 아니라, 캐시를 초기화
    • 초기화 한 후에 받아온 데이터를 캐시에 저장
const revalidate = async () => {
    "use server";
    // "/"(홈)의 레이아웃 아래에 있는 모든 컴포넌트의 캐시 삭제
    // = 사실상 모든 컴포넌트의 캐시를 삭제하는 방법
    await revalidatePath("/", "layout");
  }}

본 게시글의 예제 코드는 수코딩(https://www.sucoding.kr)을 참고했습니다