Next.js 14 기초 정리

D uuu·2024년 5월 2일
1

Next.js

목록 보기
1/5
post-thumbnail

Next.js 14 기초

노마드코더에 무료 Next.js 14 강의가 올라와서 강의를 듣고 기본 개념과 동작 방법에 대해 정리해보는 시간을 가졌다.

[출처] 노마드코더 - Next.js 시작하기
https://nomadcoders.co/nextjs-for-beginners

Next.js Router 방식

oldnew
page routerapp router or page router (둘다 사용 가능)

실행할 때 app 폴더 안의 page 라는 파일이 진입점

app 폴더 > page.jsx -> 진입점 (root)
예시) http://localhost:3000/

app 폴더 > about-us 폴더 > page.jsx 새로운 페이지 생성
예시) http://localhost:3000/about-us

없는 경로로 접근할경우 대응하기

/app/not-found.jsx(tsx) 파일을 생성하면 없는 경로로 접근할경우 자동으로 not-found 컴포넌트가 렌더링 된다.
⭐️ 이때 파일 이름은 꼭 not-found 이여야 하며, page.jsx 와 같은 위치에 있어야 한다.

usePathname 훅 사용하기

현재 위치한 path 를 알고 싶으면 usePathname 훅을 사용하면 된다.

const path = usePathname();
console.log(path);  /// about-us

그런데 이때 "use client" 를 사용하라는 에러 메시지가 뜨는데, 컴포넌트 상단에 "use client" 를 입력해주면 된다.

그렇다면 왜 'use client' 를 입력하면 에러가 고쳐지는에 대해 자세히 알아보자.


CSR & SSR

⭐️client side rendering

렌더링은 react 코드를 브라우저가 이해할 수 있는 html 코드로 바꾸는 작업을 말한다.

실제 CRA 로 실행한 리액트 소스코드를 보면 아래와 같이 비어 있는 걸 볼 수 있다.

하지만 아래 사진을 보면 작업관리자 창에서 요소를 보면 div, h1 태그를 볼 수 있는데, div, h1 태그는 브라우저가 자바스크립트 파일을 다운로드 하고 실행 한 후에야 보여지는 것이지 처음부터 존재하는게 아니다.

즉, 유저가 페이지를 처음 딱 진입하는 그 시점에 해당 페이지는 위 사진처럼 아무것도 없는 빈 페이지를 보게 된다.

그래서 새로고침을 하면 잠깐동안 화면에 아무것도 보이지 않는 이유가 실제로 브라우저가 화면에 렌더링 할 UI 가 없기 때문이다.

client side rendering 은 모든 렌더링, 즉 모든 UI 구축 작업이 client 측에서 일어나는걸 말한다. (client 는 JavaScript 를 로드하고, 그 후에 JavaScript 가 UI 를 빌드한다.)

그렇기 때문에 생기는 단점은 아래와 같다.

  1. 만약 사용자가 데이터 연결 상태가 안좋은곳에서 연결을 시도할때, 자바스크립트가 모두 다운 되는 동안 빈 화면을 보게 될 수 있고 결국 사용자 경험에도 안좋은 영향을 줄 수 있다.

  2. 검색 엔진이 페이지를 진입했을때 빈 html 파일을 보게 되므로 SEO 검색 엔진 최적화에도 좋지 못하다.

⭐️server side rendering

Next.js 에서는 기본적으로 server side rendering 을 지원한다.

Next.js 로 구현한 소스 코드를 보면 react 와 다르게 페이지의 내용들이 모두 브라우저 코드에 있는 걸 볼 수 있다.

이 말은 즉슨, 브라우저가 JavaScript 가 로드될 때까지 기다릴 필요없이 바로 화면에 그릴 html 이 있다는 뜻이된다.

CSR 과 비교해봤을때, SSR 의 소스코드를 보면 html 코드가 들어있는 걸 볼 수 있다.

⭐️기억해야할 것은 Next.js 의 모든 컴포넌트들은 SSR 으로 동작한다는 것이다.

client components , sercer components, 상단에 'use client' 를 입력한 컴포넌트던 상관없이 모두 기본적으로는 SSR 로 동작한다.

헷갈리면 안되는 게 'use client' 라고 적혀있어서 CSR 이라고 착각할 수 있는데 이 역시 기본적으로 SSR 로 동작한다는 점이다.

Next.js 로 build 된 페이지에 접속하면 기본적으로 backend 에서 렌더링 되고 Next.js 가 코드를 html 로 변환하고 그걸 브라우저에게 넘겨주면 사용자는 그 화면을 보게 된다

Hydration

서버 측 렌더링(SSR)된 페이지의 초기 HTML 마크업이 클라이언트 측 자바스크립트에 의해 다시 렌더링되거나 재생성되는 프로세스를 말한다. 이것은 일반적으로 클라이언트 측 렌더링(CSR)으로의 전환을 통해 이루어진다.

Next.js 에서는 일반적으로 서버 측 렌더링을 통해 초기 페이지를 생성하고, 그런 다음 클라이언트 측 자바스크립트(리액트)로부터 이 페이지를 다시 hydration하여 인터랙티브한 요소와 상호작용을 추가한다.

이에 대한 예시로 a 태그를 클릭해 페이지를 이동할때 새로고침이 전혀 일어나지 않는 것을 볼 수 있다.

이는 초기 SSR 된 페이지에서 이후 hydration 과정을 거쳐 클라이언트 측 렌더링으로의 전환이 이루어졌기 때문이다.

따라서 Link 태그를 통해 페이지 전체를 reload 하지 않고 빠르게 페이지를 이동할 수 있다는 것을 의미한다.

예시)
사용자가 http://localhost:3000/count 로 이동
Next.js 는 요청을 보고 서버측에서 컴포넌트를 html 로 변환
이때 사용자는 단순한 0 버튼만 보여짐
그러고 나서 hydration 과정을 통해 클라이언트 측 렌더링으로의 전환이 이루어지면, onClick eventListener 가 연결된 버튼이 되어 interaction 이 가능해져 버튼을 누르면 숫자가 1씩 더해진다.

'use client'

다시 돌아와서 지금껏 'use client' 를 설명하기 위한 빌드업 단계였다.

컴포넌트 상단에 'use client' 지시어를 입력하면 그 컴포넌트는 hydration 를 진행하는 컴포넌트가 된다.

위 두 컴포넌트는 모두 SSR 된다.

그러나 AboutUs 컴포넌트는 hydration 되지 않고, Navigation 컴포넌트는 상단에 'use client' 지시어가 있으므로 hydration 된다.

이것은 AboutUs 컴포넌트의 h1 태그가 단순한 HTML 라면, Navigation 컴포넌트의 태그들은 hydration을 거쳐 Link 태그를 통해 페이지 전체를 다시 로드하지 않고 빠르게 페이지를 이동할 수 있는 클라이언트 상호작용 컴포넌트가 됨을 의미한다.

따라서 Next.js를 사용할 때 컴포넌트에 'use client' 지시어를 사용하여 해당 컴포넌트가 hydration이 필요한지 여부를 Next.js에게 알려주어야 한다. 이렇게 하면 Next.js는 불필요한 자바스크립트 파일을 다운로드하지 않고 로딩 속도를 빠르게 할 수 있게 된다.

Layout

Next.js를 실행하면 먼저 레이아웃 파일이 있는지 확인하고, 그렇지 않으면 디렉토리에 레이아웃 파일을 자동으로 생성한다.

여러 컴포넌트에서 반복적으로 사용되는 header, footer 등과 같은 레이아웃을 반복적으로 작성하지 않고도 레이아웃 컴포넌트를 활용하면 모든 컴포넌트에 동일한 레이아웃을 적용할 수 있다.

레이아웃은 각 폴더 단위로 설정할 수 있다.
예를 들어, 루트 디렉토리에 layout.jsx를 생성하거나 app/about-js/layout.jsx를 생성하면, about-js 폴더 내의 파일들은 루트 디렉토리에 설정한 레이아웃과 about-js 폴더에 설정한 레이아웃이 모두 적용된다.

metadata

Next.js 에서는 별도의 라이브러리 설치나 작업 없이 metadata 객체를 통해 손쉽게 metadata 를 내보낼 수 있다.

단, page, layout 에서만 metadata 를 내보낼 수 있다.
일반 컴포넌트에서는 metadata 를 내보낼 수 없고 또 서버 컴포넌트에서만 작성할 수 있다.

아래 사진을 보면 링크를 이동할때마다 title 부분이 바뀌는걸 볼 수 있다.

이런식으로 각 page 와 layout 에 metadata 를 설정할 수 있다.
metadata 의 옵션을 사용해 반복적으로 들어가는 문구를 template 통해 설정할 수 있고, metadata 가 없는 페이지에 들어갈경우 기본값으로 보여질 title 도 설정할 수 있다.
이 외에도 많은 옵션들이 있다. (공식 문서에서 확인)

decription 은 모든 컴포넌트에 동일하게 적용되기에 layout 컴포넌트에만 작성했다.

export const metadata = {
    title: 'Home',
};

export default function Home() {
    return (
        <div>
            <h1>Hello!</h1>
        </div>
    );
}

export const metadata = {
    title: 'About Us',
};

export default function AboutUs() {
    return (
        <div>
            <h1>About us ! </h1>
        </div>
    );
}

export const metadata = {
    title: {
        template: '%s | Next Movies',
        default: 'Next Movies',
    },
    description: 'The best movies on the best framework',
};

export default function RootLayout({ children }) {
    return (
        <html lang="ko">
            <body>
                <Navigation />
                {children}
            </body>
        </html>
    );
}

dynamic routes

Next.js를 사용하여 동적으로 라우팅을 구현할 때는 대괄호([])를 사용한다.

예를 들어, [id]와 같이 대괄호 안에 경로 파라미터를 넣어줄 수 있다. 이렇게 하면 해당 경로로 접속할 때 동적으로 id 값을 받아와서 해당 데이터를 렌더링한다.

/app/movies/[id]/page.jsx 와 같이 파일을 생성
예시) http://localhost:3000/movies/12345 경로로 접속 가능

이때 id 값을 가져오기 위해서는 컴포넌트의 props 에 있는 params 속성을 통해 현재 id 값을 가져와서 사용할 수 있다.

export default function MovieDetail({ params: { id } }) {
    console.log(id);

    return <h1>Movie</h1>;
}

fetch

React-query 와 같은 라이브러리를 사용하지 않고 useState와 useEffect를 이용하여 API 요청을 보내는 경우, 일반적으로 아래와 같이 작성한다.

'use client';

import { useEffect, useState } from 'react';

export default function HomePage() {
    const [movies, setMovies] = useState([]);
    const [isLoading, setIsLoading] = useState(true);

    const getMovies = async () => {
        const response = await fetch(`${baseUrl}/movies`);
        const result = await response.json();
        setMovies(result);
        setIsLoading(false);
    };

    useEffect(() => {
        getMovies();
    }, []);

    return <div>{isLoading ? <div>Loading...</div> : JSON.stringify(movies)}</div>;
}

Next.js 에서 위와 같이 작성할 경우에는 'use client' 를 상단에 작성해줘야 한다.

이렇게 하면 이 컴포넌트는 서버에서 초기 렌더링을 수행한 후 클라이언트에서 hydration을 통해 동작한다.

따라서 이 컴포넌트에서는 metadata를 사용할 수 없다. 왜냐하면 metadata는 서버 측에서만 작성할 수 있기 때문이다.

💙정리 > 이 컴포넌트는 초기에는 서버 측 렌더링(SSR)으로 동작하며, 클라이언트 측에서 hydration이 이루어지면서 동적으로 데이터를 불러와 화면에 표시하는 클라이언트 측 컴포넌트가 된다.


동일한 코드를 Next.js 를 활용해 작성하면 아래와 같다.

export const metadata = {
    title: 'Home',
};

async function getMovies() {
    const response = await fetch(`${baseUrl}/movies`);
    const result = await response.json();
    return result;
}

export default async function HomePage() {
    const movies = await getMovies();

    return <div>{JSON.stringify(movies)}</div>;
}

서버에서 getMovies 함수를 실행시키고 요청을 기억하고 있다가 HomePage 컴포넌트에서 값을 받아서 사용할 수 있다.

때문에 HomePage 컴포넌트는 서버에서 API 요청 후 데이터를 받아오는 것을 기다려야 하기 때문에 aysnc 로 감싸줘야 한다.

위 컴포넌트는 서버 컴포넌트이기 때문에 metadata 를 사용할 수 있다.

그렇다면 데이터를 불러오는 동안에 사용자에게 로딩 중임을 나타내는 UI 를 띄우기 위해서는 어떻게 해야할까?

이런식으로 app/(Home)/loading.jsx 를 만들면 별도의 로직을 작성하지 않아도 프레임워크가 서버에서 데이터를 불러오는 동안에 미리 작성한 loading.jsx 파일을 렌더링해 화면에 띄어준다.

⭐️여기서 주의할점은 파일 이름은 꼭 loading 이어야 한다는 것과 page 파일 옆에 위치해야 제대로 동작한다는 점이다.

여기서 (Home) 식으로 괄호() 안에 폴더명을 작성하면 해당 폴더는 url 로 동작하지 않는다.
이를 이용해서 URL 경로 구조에 영향을 주지 않고 프로젝트 파일을 깔끔하고 체계적으로 정리할때 사용할 수 있다.


fetch 응용

API 요청을 연달아 보낼때 순서가 중요한지 아닌지를 잘 따져봐야 한다.

아래 코드는 getMovie 먼저 실행되고 나서 getVideo 가 실행된다.

만약, getMovie 는 5초가 걸리고 getVideos 는 10초가 걸린다고 할때 모두 수행하는데 총 15초가 걸린다.

async function getMovie(id) {
    console.log(`Fetching movies : ${Date.now()}`);
    await new Promise((resolve) => setTimeout(resolve, 5000));
    const response = await fetch(`${baseUrl}/${id}`);
    return response.json();
}

async function getVideos(id) {
    console.log(`Fetching videos : ${Date.now()}`);
    await new Promise((resolve) => setTimeout(resolve, 10000));
    const response = await fetch(`${baseUrl}/${id}/videos`);
    return response.json();
}


    console.log('start fetching');
    const movie = await getMovie(id);
    const videos = await getVideos(id);
    console.log('end fetching');

콘솔을 보면 getMovie 함수가 실행되고 나서 5초후에 getVideos 함수가 실행된걸 확인할 수 있다.

start fetching
Fetching movies : 1714635331223
Fetching videos : 1714635336627
end fetching

하지만 위 코드를 보면 어떤 함수가 먼저 실행되어도 뒤에 영향을 전혀 주지 않는다. 따라서 순서가 중요하지 않기 때문에 Promise.allSettled 를 사용해 getMovie, getVideos 를 동시에 실행되도록 수정해줬다.

    console.log('start fetching');
    const [movie, videos] = await Promise.allSettled([getMovie(id), getVideos(id)]);
    console.log('end fetching');

아래 콘솔을 다시 보면 getMovie 와 getVideos 함수가 동시에 실행되었음을 알 수 있다.

마찬가지로 getMovie 는 5초가 걸리고 getVideos 는 10초가 걸린다고 할때 결국 수행하는데 걸리는 시간은 10초가 된다.

start fetching
Fetching movies : 1714636375110
Fetching videos : 1714636375110
end fetching

이런식으로 구현하면 movie, videos 데이터를 모두 불러오는데 성공했을때 (약 10초 후에) 아래와 같이 movie, videos 데이터가 화면에 보여진다.

하지만, Promise.allSettled 를 사용하면 데이터를 모두 불러오는 동안 빈 화면이나 로딩 화면만 보이게 된다.

이는 사용자 경험에 좋지 않은데 따라서 먼저 받아온 데이터를 화면에 표시하도록 수정해보자.

Suspense 활용하기

기존 한 컴포넌트에서 작성했던 로직을 movie-info, movie-videos 컴포넌트로 나눠서 작성하고 React Suspense 기능을 활용해 컴포넌트를 감싸줬다.

그리고 각 데이터를 불러오는 동안에 보여질 fallback 도 설정해줬다.

이런식으로 Suspense 를 활용하면 로딩 상태를 분리해 관리할 수 있다는 장점이 있다.

export default function MovieDetail({ params: { id } }) {
    return (
        <div>
            <Suspense fallback={<h1>Loading movie info</h1>}>
                <MovieInfo id={id} />
            </Suspense>
            <Suspense fallback={<h1>Loading movie videos</h1>}>
                <MovieVideos id={id} />
            </Suspense>
        </div>
    );
}

이렇게 구현했을때, 먼저 데이터를 불러오는데 성공한 movie 데이터가 먼저 화면에 나타나고 그 다음 videos 데이터가 화면에 나타난다.

따라서 사용자는 데이터를 기다리는 동안 fallback 으로 설정한 로딩 UI 를 볼 수 있고, 먼저 데이터를 불러오는대로 화면에 데이터가 나타나기 때문에 기존 코드보다 기다리는 시간이 적다.

Error Handling

API 요청에 실패하는 등 에러가 발생했을때 처리 방법도 간단하게 구현할 수 있다.

/app/movie/[id]/error.jsx 파일을 생성하면 같은 경로에 있는 page.jsx 에서 에러가 발생하면 error.jsx 컴포넌트가 렌더링 된다.

⭐️ 이때 파일 이름은 꼭 error 이여야 하며 page.jsx 와 같은 위치에 있어야 한다.

error 컴포넌트 상단에 'use clinet' 를 작성해줘야 한다.
error 컴포넌트는 error, reset 객체를 props 으로 받는다.

에러 핸들링 공식문서 바로가기

'use client';

export default function ErrorOMG({ error, reset }) {
    return (
        <div>
            <h3>Something error...</h3>
            <button onClick={() => reset()}>Try again</button>
        </div>
    );
}
profile
배우고 느낀 걸 기록하는 공간

0개의 댓글

관련 채용 정보