본문 바로가기
TIL

Next.js - 노마드 코더[CSR,SSR / SPA / DOM]

by 잼민타치 2024. 2. 11.

본 내용은 노마드 코더의 Next.js 14 를 공부하면서 모르거나 헷갈리는 부분들을 정리한 내용입니다.

따라서 일부의 내용이 생략되어 있을 수 있으며, 필자의 개인적 생각들이 첨가되어 있을 수 있으며

틀린 내용들이 있다면 언제든 지적해주시면 감사하겠습니다.

 

 

 

 

-----------------------------------------------------------------------

 

라이브러리와 프레임워크에 대한 이해

 

React = 라이브러리

원하는 기능이 있을 때 그냥 가져다가 자유롭게 쓰는 것이다. 결정권이 우리에게 있으며 우리가 라이브러리를 사용하는 것이다.

 

 

Next.js = 프레임워크

Next.js의 기능을 사용하고 싶으면 Next.js의 룰을 따라야 한다. 우리가 제대로 된 위치에 코드를 구현했다면 next.js가 이 코드를 사용해서 프로덕트를 만들게 될 것이다.

(예를 들어 코드 에디터에서 npm run dev를 하게 되면 next dev라는 명령어가 실행되고, 그러면 app에 있는 page.tsx 파일을 참조하게 될 것이다.)


 

 

 

 

 

SPA, MPA

MPA는 Multi page application의 줄임말이며 SPA는 Single page application의 줄임말이다.

 

먼저 MPA로 웹을 제작하게 되면 link tag(a href="")를 통해 전통적인 웹페이지의 링크방식으로 구현하게 된다.

하나의 어플리케이션에서 여러 개의 페이지를 가지게 되는 것이다.

이렇게 되면 클라이언트가 href attribute 값인 리소스 경로가 URL의 path에 추가되어서 주소창에 나타나고 해당 리소스를 서버에 요청하게 된다. 이 때 서버는 즉시 렌더링 가능한 HTML 파일을 만들어 클라이언트에 보낸다. 이를 서버 사이드 렌더링 (SSR)이라고 한다. 만약 유저가 버튼을 누르거나, 폼을 제출하거나 하면 서버에 request를 보내고 response를 받은 클라이언트 브라우저는 전체 페이지를 다시 렌더링하게 되므로 새로고침이 발생한다.

 

SSR의 구현 방식, 출처 : https://poiemaweb.com/js-spa

 

이렇게 매번 새로고침을 하는 문제를 해결하기 위해 다양한 기술이 등장했다.(AJAX, Javascript의 fetch API, Axios와 같은 서드파티 라이브러리 등. 이에 대한 설명은 여기를 참고.)

 

이러한 기술을 활용한 SPA(Next.js와 React 모두 이 SPA 개발에 사용되는 프레임워크와 라이브러리다.)는 웹 애플리케이션에 필요한 모든 정적 리소스를 최초 접근시 한번만 다운로드 하게 된다. 이 후 새로운 페이지를 요청하게 됐을 시, 페이지 갱신에 필요한 데이터만 JSON으로 전달받아 페이지를 갱신하므로 전체 트래픽을 감소시킬 수 있으며 이미 로드된 페이지에서 DOM 내용만 교체하기 때문에 MPA처럼 화면이 깜빡이거나 하는 사용자 경험을 개선할 수 있다.


 

 

 

 

라우팅

React는 url에 따라 '어떤 컴포넌트를 불러와주세요' 라며 페이지를 생성했다.

Next.js는 파일시스템을 통해 url을 표현한다.

즉 app이라는 폴더 안에 about-us라는 폴더를 생성하고, 그 안에 page.tsx를 만들어 ui를 꾸며주면

기본 홈에서 /about-us라는 url로 가면 이 ui가 렌더링 되는 것이다.

 

또 Next.js는 not-found.tsx 파일을 앱에 만들어줌으로써 정의되지 않은 url로 가려 할 때 나오는 페이지를 커스텀 가능하다.

(쉽게 말하면 404 not found 페이지를 커스텀 하는 것. 이 기능은 React-router-dom에서도 가능하다.)

 

네비게이션 컴포넌트를 만들면서 라우팅에 대해 자세히 알아보자. 우리는 이 네비게이션을 만들 때 ul의 li들에게 각각 a href=”" 이런식으로 만들진 않을 것이다. 이건 브라우저의 네비게이션을 사용하는 MPA방식이며 우리는 프레임워크의 네비게이션을 사용하여 SPA를 개발할 것이다.

 

 

 

export default function Navigation() {
  const path = usePathname();
  return (
    <nav>
      <ul>
        <li>
          <Link href="/">Home</Link> {path === "/" && "(current)"}
        </li>
        <li>
          <Link href="/about-us">About Us</Link>{" "}
          {path === "/about-us" && "(current)"}
        </li>
      </ul>
    </nav>
  );
}

바로 이런 식으로 작성해주면 된다. 여기서 usePathname은 현재 내가 어떤 주소에 있는지를 보여주기 위해 가져온 훅이다.

(다만 이 때는 “use client” 를 코드 맨 위에 작성해주어야 한다. 이러한 차이점은 추후 서술)


 

 

 

 

 

렌더링

간단히 말하면 웹 페이지의 HTML, CSS, JS 등의 코드를 실제로 사용자의 웹 브라우저에 시각적으로 표시하는 과정을 의미한다.

 

1. 먼저 브라우저는 서버로부터 받은 HTML 파일을 읽기 시작한다. 이 파일은 웹 페이지의 구조를 정의한다. 브라우저는 HTML 태그를 파싱해서 DOM트리를 구축한다. DOM 트리는 웹 페이지의 모든 요소와 속성, 콘텐츠를 포함하는 것을 의미한다. (추후 서술)

 

2. 동시에 CSS파일을 파싱해서 CSSOM트리를 구축한다. 이는 웹 페이지의 모든 스타일 정보를 포함한다.

 

3. DOM과 CSSOM트리가 결합되어 렌더 트리를 형성한다. 렌더 트리는 실제로 화면에 그려질 요소들만을 포함하며 스타일 정보를 적용한 시각적 요소의 구조를 나타낸다.

 

4. 렌더트리가 준비되면 브라우저는 각 요소가 화면의 어느 위치에 나타나야 하는지 계산하는 레이아웃 과정을 거친다. 이 과정에서 각 요소의 정확한 크기와 위치가 결정된다.

 

5. 마지막 단계에서 레이아웃에 따라 실제로 요소들을 화면에 그리는 페인팅 작업이 이루어진다.

 

 

 

 

동적 렌더링

렌더링에는 정적 렌더링과 동적 렌더링이 있다.

웹 어플리케이션에서 사용자와의 상호작용이나 데이터의 변경에 따라 Javascript를 사용해 동적으로 DOM을 수정하고 이러한 변경사항을 렌더링 할 수 있다. 이를 동적 렌더링이라고 한다.

 

예를 들어, 사용자가 어떤 버튼을 클릭했을 때 새로운 데이터를 보여주는 경우, Javascript는 해당 데이터를 서버로부터 불러온 후 DOM을 업데이트하고, 이 변경사항이 렌더링 과정을 거쳐 사용자에게 표시된다.

 

 

 

 

 

위에서 언급한 SSR(서버사이드 렌더링, 문서의 전체구조를 미리 생성하여 HTML,CSS,데이터가 서버에서 완성된 형태로 렌더링 되어 사용자의 브라우저로 전송되는 것)이 아니라,

 

CSR(클라이언트 사이드 렌더링)을 하게 되면 클라이언트는 사용자 브라우저에서 모든 렌더링 작업을 수행해야 한다.

즉, HTML 문서가 서버로부터 사용자의 브라우저로 전송될 때 최소한의 HTML 구조만 포함되어 있으며 대신 자바스크립트 파일이 함께 전송되어 사용자의 브라우저에서 직접 자바스크립트 파일을 실행하게 된다.

이 자바스크립트가 브라우저에서 DOM을 동적으로 생성하고, 필요한  데이터를 서버로부터 비동기적으로 요청하여 받아온 후 웹 페이지를 구성하여 렌더링하도록 한다.


 

 

 

 

 

 

자 지금까지 MPASPA, CSRSSR에 대해 알아보았다.

 

근데 여기서 드는 근본적인 의문.

 

이 많은 걸 제가 왜 알아야 하나요 ..?

 

 

 

 

 

 

 

=> 왜냐면 React와 Next.js는 동적 렌더링이 가능한 Javascript의 불편한 점을 개선시키려고 등장한 도구들이기 때문이다.

뭐가 불편한지 알아야 왜 쓰는지 알겠죠?

 

 

 

 

 

 

 

 

React는 위에서 SPA의 장점에서 언급한대로,

 

(이 후 새로운 페이지를 요청하게 됐을 시, 페이지 갱신에 필요한 데이터만 JSON으로 전달받아 페이지를 갱신하므로 전체 트래픽을 감소시킬 수 있으며 이미 로드된 페이지에서 DOM 내용만 교체하기 때문에 MPA처럼 화면이 깜빡이거나 하는 사용자 경험을 개선할 수 있다.)

 

이기 때문에 사용하게 되었다. 

 

 

 

 

 

 

그러나 리액트는 CSR 방식을 사용하기 때문에 

다음과 같은 단점들이 존재하게 되는데,

 

 

 

 

1. 사용자 경험의 악영향

만약 사용자가 터널이나 엘리베이터처럼 데이터 환경이 나쁜 곳이라면 모든 자바스크립트 파일을 다운 받고, 모든 파일이 다운로드 완료 될 때까지 빈 화면을 보는 경험을 오래 지속 하게 된다.

 

2. SEO (검색 엔진 최적화)가 되지 않는다.

검색엔진은 웹페이지의 html을 보고 나서, 어떤 내용이 들어있는지를 검색 사용자에게 보여주게 되는데, 이렇게 CSR을 하게 되면 유저에게 빈페이지를 보여주게 된다. 구글은 자바스크립트를 실행해서 검색한 사람들에게 보여주기도 한다지만 너무 리스크가 크다.

(사실 리액트에서도 SEO 를 하는 방법이 있다고는 합니다. 다만 조금 어려울 뿐)

 

 

 

 

 

 

 

이를 해결하기 위해 등장한 Next.js는

CSR에 SSR을 합쳐버린다.

 

 

 

 

오.. 어떻게요?

 

먼저 링크에 유저가 접속하면 html을 준다.(SSR 방식 or SSG 방식)

그리고, 유저가 어떤 상호작용이 있기 이전에 우린 뒤에서 프레임 워크(여기선 Next.js)를 로드하고 리액트 애플리케이션을 초기화 하여 interactive 하게 만들어준다.(CSR 방식)

 

그래서 나중에는 네비게이션에서 링크를 클릭하여 다른 링크로 움직인다고 가정했을 때, 리액트 애플리케이션이 초기화 되지 않았다면 전체 페이지가 새로고침 되어야 했던 것들이 (hard refresh) 이제는 부분적으로 바뀌면서 새로고침이 일어나지 않게 된다.

 

 

 

 

단순 HTML을 react application으로 초기화 하는 작업, interactive 하게 만드는 이 작업을 우리는 hydration 이라고 부른다. 서버사이드 렌더링(SSR)과 스태틱 사이트 제너레이션(SSG)은 모든 컴포넌트에 대해 일어나지만

(이렇게 Next.js가 브라우저에 렌더링 할 때 기본적으로 하는 것들을 pre-rendering 이라고 한다.)

 

이러한 하이드레이션은 코드 맨 위에 “use client”라고 적힌 컴포넌트에 대해서만 일어나게 된다. 따라서 useState를 써서 인터랙티브하게 만든 컴포넌트에 대해서는 코드 초기에 “use client”라고 쓰지 않으면 문제가 발생하게 된다.

 

⇒ use client라고 씀으로써 backend 서버에서 Render 하고,

프론트엔드에서 인터랙티브하도록 만들어!!를 지시한 것이다.


 

 

 

 

 

 

결론 : Next.js는 백엔드에서 우리의 앱을 프리 렌더한다. 그래서 모든 컴포넌트를 가져가서 non-interactive한 HTML로 바꿀 것이다. 그리고 그걸 사용자에게 준다. 그리고 나서 프레임워크와 React.js를 initialize 한다. 그러고 난 뒤 'use client' 명령어를 가진 컴포넌트만 자바스크립트를 실행하게 된다.

 

⇒ 이렇게 해줌으로써 사용자들이 받아야 할 자바스크립트 코드들이 줄어든다. interactive한 자바스크립트만 실행하면 되니까.

따라서 CSR의 단점( 처음에 로딩 느린 것, SEO 검색엔진 최적화 )과 기존 MPA에서의 SSR 단점( 매 응답시 새로고침 해야되는 것 )

모두 해결한 것이 NEXT.JS 이다 !

 

 

 

 

 

잠깐, 근데 도중에 우리가 못본 SSG 방식이라는게 있는데요..?

맞습니다 ㅎㅎ

사실 next.js는 pre-rendering 할 때 동적으로 해서 페이지를 생성하느냐, 정적으로 페이지를 생성하느냐에 따라

SSR과 SSG로 나눠서 작동하도록 할 수 있습니다.

 

 

SSG는 작성한 각 페이지들에 대해 각각의 문서를 생성해서 static한 파일로 생성하기에 정적 생성된 정보를 요청에 동일한 정보로 반환하는 블로그 게시물, 제품 목록, 마케팅 페이지 같은 곳에서 사용하고,

 

 

SSR유저가 페이지를 요청할 때마다 그에 맞는 HTML 문서를 생성해서 반환합니다. 항상 최신 상태를 유지해야 하는 웹 페이지, 주식 차트, 게시판 등 사용자의 요청마다 동적으로 페이지를 생성해 다른 내용을 보여주어야 하는 경우에 해당합니다.


 

 

 

 

 

 

 

Layout

네비게이션 바 같이 모든 페이지에 중복되는 컴포넌트들은 복사 붙여넣기 하기 귀찮지않나? 그러므로 layout.tsx 파일을 만들어서 수정하면 된다. 또한 레이아웃은 중첩된다. 그래서 app에 존재하는 layout.tsx랑 그 앱에 하위에 있는 about-us 폴더에 있는 layout.tsx가 모두 웹에서 표현된다.

 

 

 

 

Routes group 

새 폴더를 만들고 폴더 이름을 괄호로 묶어주면 URL에는 영향을 끼치지 않으면서 페이지들을 이쁘게 관리할 수 있다.

또한 metadata에서 title이나 description을 바꿀 수 있지만 use client 를 쓰는 컴포넌트에서는 불가능하다

메타데이터는 중첩되는 레이아웃 처럼 병합될 수 있는데, 이 성질 때문에 기본 layout.Tsx 에서는

export const metadata: Metadata = {
  title: {
    template: "%s | Next movie",
    default: "Next movie",
  },
  description: "The best movies on the best framework.",
};

이렇게 쓰고, 각각의 페이지에서 메타데이터를 내보낼 때

 

 

export const metadata: Metadata = {
  title: "About us",
};
export const metadata = {
  title: "Home",
};

이렇게 바꾸기 가능하다. 참고로 맨 처음에 layout.tsx에서의 default는 이렇게 메타데이터를 주는 url이 아닌 다른 url에 들어가게 되면 저 default 값이 보여지게 된다는 것이다.

 

 

 

 

 

 

 

또한 동적 라우팅을 매우 쉽게 할 수 있는데, 그냥 폴더에 url 뒤에 올 것을 [] 안에 적어주면 된다.

예를 들어, movies 폴더 안에 [id]라는 이름의 폴더를 놓고, 그 안에 page.tsx를 만들어서

export default function MovieDetail({
  params: { id },
}: {
  params: { id: string };
}) {
  return <h1>Movie {id}</h1>;
}

이렇게 만들어 줄 수 있는 것이다.

 

만약 코드를

 

export default function MovieDetail(props) {
  console.log(props);
  return <h1>Movie </h1>;
}

이렇게 짜주면 프론트엔드 콘솔창이 아닌 서버 터미널에서

{ params: { id: '1234' }, searchParams: {} }

이러한 값이 나오게 된다.즉 클라이언트쪽이 아닌 서버사이드, 백엔드에서 렌더링 된 것이다.

 

 

 

 

 

 

부연 설명들

1. (당연히 이때 들어간 주소는 http://localhost:3000/movies/1234이다.) 그러니 params의 id가 저렇게 나오게 된 것.

 

 

2. (searchparams는 주소창에 1234 뒤에 region=kr&page=2 를 붙여 주면 searchparams에 Region : kr, page: 2 가 들어가게 된다. 이건 나중에 내가 만든 웹 페이지에서 검색엔진을 만들 때 유용하게 쓰인다.)

 

 

3. (function MovieDetail({ params: { id } }) { // 함수 내부에서 id 사용 }

자바스크립트의 구조분해 할당을 의미한다. 만약 MovieDetail 이라는 함수가 호출 되면 첫번째 인자로 전달된 객체에서 Params라는 키를 찾아서 그 안의 id 값을 매개변수로 사용할 수 있도록 한다.

 

 

4. ({ params: { id } }: { params: { id: string } })

만약 타입스크립트를 쓰고 있다면 이렇게 써야 하는데, 왜냐하면 params 객체가 반드시 id라는 문자열 타입의 속성을 가져야 함을 명시하기 위함이다. 즉, MovieDetail 함수는 id라는 문자열을 포함한 params 객체를 인자로 받아야 함을 정의한 것이다.)


 

 

 

 

 

 

데이터 페칭

자 이제 React만을 사용하여 api 서버에서 데이터를 fetch 하는 코드를 짜보자.

(비동기처리와 서버와의 데이터 통신에 대한 글은 여기를 참고할 것)

 

"use client";

import React, { useState, useEffect } from "react";

export default function Page() {
  const [isLoading, setIsLoading] = useState(true);
  const [movies, setMovies] = useState([]);
  const getMovies = async () => {
    const response = await fetch(
      "<https://nomad-movies.nomadcoders.workers.dev/movies>"
    );
    const json = await response.json();
    setMovies(json);
    setIsLoading(false);
  };
  useEffect(() => {
    getMovies();
  }, []);

  return
  {isLoading ? "Loading..." : JSON.stringify(movies)}
;}

 

 

 

이런식으로 코드를 작성해야 할 것이다.

 

여기서 질문. 왜 useState 같은 상태를 관리하는 도구가 있어야 페칭해올 수 있나요?

  1. 데이터를 페칭하기 전에 setIsLoading과 같은 상태관리 기능이 없다면 데이터를 로딩하는 동안 사용자에게 로딩 상태를 보여줄 방법이 없다.
  2. 데이터를 페칭한 후에 데이터가 로드 되었을 때 즉시 UI를 업데이트할 수 없다. React 컴포넌트의 렌더링을 다시 트리거하지 않기 때문에 사용자는 최신 데이터를 볼 수 없게 된다.

 

 

그럼 상태를 쓰지 않고 하면 되지 않나요? 그런 방법은 없나요?

 

DOM 직접 조작: 순수 JavaScript를 사용하여 DOM을 직접 조작하면 상태 관리 없이 UI를 업데이트할 수 있긴 하다.

예를 들어, 데이터 로딩이 완료되면 JavaScript를 사용하여 직접적으로 HTML 요소의 내용을 변경할 수 있다. 그러나 이 방법은 React의 선언적 UI 패러다임과 맞지 않으며, React의 가상 DOM과 실제 DOM 사이의 동기화 문제를 일으킬 수 있다.

 

 

 

 

 

부연 설명들.

DOM이란?

DOM은 Document Object Model의 약자로, 웹 페이지의 HTML 문서를 프로그램이 읽고 수정할 수 있는 구조이다. 쉽게 말해, 웹 페이지의 모든 요소(텍스트, 이미지, 버튼 등)를 나타내는 코드의 트리(나무처럼 뻗어 나가는 구조)를 말한다. JavaScript를 사용하면 이 DOM을 수정하여 페이지의 내용이나 스타일을 동적으로 변경할 수 있다.

 

 

 

DOM 직접 조작이란?

DOM 직접 조작이란, JavaScript를 사용하여 웹 페이지의 HTML 요소를 직접 변경하는 것을 말한다. 예를 들어, JavaScript 코드로 특정 요소를 찾아서 그 내용을 '로딩 중...'에서 실제 데이터로 바꾸는 것이다. 이 방법으로, 페이지에 있는 텍스트나 이미지 등을 프로그램적으로 변경할 수 있다.

 

 

React에서 DOM 직접 조작은 왜 문제인가요?

React는 '선언적' 방식으로 UI를 구성한다. 즉, 우리는 어떤 상태에서 UI가 어떻게 보여야 하는지를 '선언'하고, 데이터가 변경될 때 React가 자동으로 UI를 업데이트하도록 한다. 이 과정에서 React는 '가상 DOM'을 사용하여 효율적으로 실제 DOM을 업데이트한다.

만약 우리가 직접 DOM을 조작한다면, React의 가상 DOM 시스템과 충돌이 발생할 수 있습니다. React는 가상 DOM과 실제 DOM 사이의 차이를 계산하여 필요한 최소한의 변경만을 실제 DOM에 적용합니다. 직접 DOM을 변경하면 React가 이러한 계산을 정확히 할 수 없게 되어, 예상치 못한 문제나 성능 저하를 일으킬 수 있습니다.

 

(더욱 자세한 리액트 개념을 원한다면 여기를 참조할 것.)

 

 

 

 

 

anyway

 

이렇게 리액트만을 사용해서 데이터 페칭이 이루어지게 되면 일단 use client를 해야하기 때문에 메타데이터도 쓸 수 없을 뿐더러

리액트 앱 <============>API<=========>데이터베이스 이렇게 통신하여야 하므로 보안상 좋지 않다.

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

const URL = "<https://nomad-movies.nomadcoders.workers.dev/movies>";

async function getMovies() {
  const response = await fetch(URL);
  const json = await response.json();
  return json;
}

export default async function HomePage() {
  const movies = await getMovies();
  return {JSON.stringify(movies)} ; }
 

 

자 이렇게 next.js의 프레임워크를 사용해서 백엔드에서 기능이 일어나도록 코드를 짜보자.

React에서의 로딩이 안 일어나게 된다. 이렇게 서버 컴포넌트를 사용하면 Fetch된 url을 캐싱시켜줄거다.

즉, 처음에 한번만 가져오면 next.js가 그것을 기억(캐싱)하기 때문에 다시 페치할 필요가 없다.

 

 

(어 그러면 계속 해서 api 요청 해야 할 땐 어떡하나요? >>> 다른 방법이 있긴 함.)

 

 

근데 로딩상태가 없어진 건 아니다. api의 첫번째 요청에 대한 응답이 느리면 로딩 상태가 길어질 것이다. 근데 이렇게 되면 아예 브라우저에 유저가 접속을 못하게 되는 불상사가 일어나기도 한다.

 

 

이를 해결하는 쉬운 방법 ⇒ page 옆에 loading.tsx를 만들어주면 된다.

그러면 서버 컴포넌트를 페칭 하는 도중에 제공 해준 로딩 파일이 페이지에 나타나게 되는 것이다. 따라서 덕분에 레이아웃과 로딩 컴포넌트는 나타나고, 아직 데이터가 페칭되지 않은 서버 컴포넌트를 기다리게 된다. 그게 서버컴포넌트가 있는 페이지의 경우 await을 써서 비동기로 홈페이지를 작성하는 이유이다.


 

 

 

 

 

 

 

자 이제 데이터를 페칭해서 API를 이용하여 웹을 구현해보자. 다음과 같이 작성할 수 있다.

import { API_URL } from "../../(home)/page";

async function getMovie(id: string) {
  const response = await fetch(`${API_URL}/${id}`);
  return response.json();
}

async function getVideos(id: string) {
  const response = await fetch(`${API_URL}/${id}/videos`);
  return response.json();
}

export default async function MovieDetail({
  params: { id },
}: {
  params: { id: string };
}) {
  const movie = await getMovie(id);
  const videos = await getVideos(id);
  return <h1>{movie.title}</h1>;
}

 

 

 

 

 

그런데 이렇게 하면 getMovie가 다 완료되어야만 getVideos가 완료되는 불상사가 일어나게 된다.

왜냐하면 비동기 작업이 순차적으로 실행되도록 하기 때문이다.

 

만약 두번째 함수가 첫번째 함수의 결과에 의존한다면 이렇게 작성하는게 맞다.
하지만 이와 같이 독립적인 기능을 하게 될 경우에는 다음과 같이 코드를 작성하여 병렬로 실행할 수 있다.

 

export default async function MovieDetail({
  params: { id },
}: {
  params: { id: string };
}) {
  const [movie, videos] = await Promise.all([getMovie(id), getVideos(id)]);
  return <h1>{movie.title}</h1>;
}

 

 

 

 

 

이렇게 수정하게 될 경우, getMovie, getVideos가 동시에 실행된다. 이게 리액트에서 병렬적으로 데이터를 페칭하는 방법이다. 

그런데 여기서 새로운 문제가 발생한다.

movie랑 videos가 둘 다 끝나야 유저에게 보여줄 수 있다는 것.

 

만약 getMovie나 getVideos 하나만 있다면, 즉 데이터 소스가 하나라면 그냥 이런식으로 페이지에서 데이터를 fetching한 뒤 사용자의 경험 개선을 위해 loading component(위에서 배운대로 loading.tsx)를 만들어주면 된다.

 

그러나 데이터 소스가 여러개라면?

일단 이 fetch 함수들을 분리하여 suspense로 만들자.

import { Suspense } from "react";
import MovieInfo from "../../../components/movie-info.tsx";
import MovieVideos from "../../../components/movie-videos.tsx";

export default async function MovieDetail({
  params: { id },
}: {
  params: { id: string };
}) {
  return (
    <div>
      <Suspense fallback={<h1>Loading...</h1>}>
        <MovieInfo id={id} />
      </Suspense>
      <Suspense fallback={<h1>Loading...</h1>}>
        <MovieVideos id={id} />
      </Suspense>
    </div>
  );
}

 

 

이게 app/movies/[id]에서 작성된 page.tsx 파일코드고,

components/movie-info.tsx와 /movie-videos.tsx는 다음과 같이 된다.

import { API_URL } from "../app/(home)/page";

async function getMovie(id: string) {
  const response = await fetch(`${API_URL}/${id}`);
  return response.json();
}

export default async function MovieInfo({ id }: { id: string }) {
  const movie = await getMovie(id);
  return <h6>{JSON.stringify(movie)}</h6>;
}
import { API_URL } from "../app/(home)/page";

async function getVideos(id: string) {
  const response = await fetch(`${API_URL}/${id}/videos`);
  return response.json();
}

export default async function MovieVideos({ id }: { id: string }) {
  const videos = await getVideos(id);
  return <h6>{JSON.stringify(videos)}</h6>;
}

 

 

 

 

이렇게 되면 두 개의 데이터 소스를 가져오는 두 개의 컴포넌트가 있을 때 각각 다르게 페칭해올 수 있으며 모두가 완료될 때 까지 기다려야 할 필요가 없어진다.

또한 각각의 컴포넌트에 따른 loading state(fallback 매개변수가 그것)도 보여줄 수 있으니 모든 장점을 다 가지게 된다.

 

이러면 이제는 movies/[id] 안에 있는 loading.tsx는 필요가 없어지게 된다. suspense는 각각의 await이며 페이지의 await과는 독립되어 있기 때문이다.


 

 

 

 

 

 

에러가 발생했을 때 해결법

error.tsx 파일을 만들어서 관리해주면 된다. 근데 만약에

이러한 구조에서 movies/[id]에 에러파일을 만들었다면 저 페이지에서 일어나는 에러만 해결 가능.

 

어 그러면 어떡하죠? ⇒ 해보니까 그냥 app 과 같은 상위 구조에 error.tsx 파일을 만들면 됩니다. 이지피지


 

 

 

 

 

 

 

CSS 모듈을 이용하여 꾸미기

 

먼저 global CSS 파일을 생성해서 애플리케이션 전체에 적용하자.

이후에 navigation.module.css 파일을 만들어준다.

그리고 그 파일 안에 .nav {속성} 등이 적힌 코드를 작성해준 다음에,

Navigation.tsx에 가서 nav 컴포넌트 옆에

className=”nav”라고 하면

절대 안된다 !

다른 파일에 nav라고 같은 클래스네임을 쓰거나 하면 충돌이 발생할 수도 있기에 그렇다.

 

 

 

 

 

 

따라서 Navigation.tsx 파일에서 .module.css 파일을 자바스크립트처럼 임포트해야 한다.

즉, import styles from “../styles/navigation.module.css”를 한 다음에,

실제 navigation.tsx 파일에서 <nav = className={styles.nav}> 이렇게 해줘야 한다는 말이다.

 

 

이러면 에러가 발생하지 않고, 이렇게 css 모듈을 하면 클래스네임이 navigation_nav____kx__6 이런 식으로

랜덤으로 잡히기 때문에 클래스의 충돌이 일어나지 않게 된다.


 

 

 

 

 

Next.js 에서의 라우팅 방법

export default function Movie({ title, id, poster_path }: IMovieProps) {
  const router = useRouter();
  const onClick = () => {
    router.push(`/movies/${id}`);
  };
  return (
    <div className={styles.movie}>
      <img src={poster_path} alt={title} onClick={onClick} />
      <Link href={`/movies/${id}`}>{title}</Link>
    </div>
  );
}

이런식으로 next.js에 존재하는 useRouter를 사용해서 라우팅을 해줄 수 있다 !

 

 

 

(어 link로 사용하는데 이거 MPA 방식의 페이지 이동 아닌가요?

=> 아닙니다. 저 Link랑 onClick은 Next.js에서 제공해주는 것이며, 페이지를 이동할 때 브라우저의 새로고침 없이 URL을 변경하여 변경된 컴포넌트만 바뀌도록 하는 겁니다.)

 

 

 

(어 근데 둘이 똑같은 기능을 하는데 왜 Link랑 onClick 따로 있나요?

=>Link 컴포넌트는 정적인 라우팅을 할 때, onClick(useRouter 훅)은 좀 더 복잡하고 동적인 라우팅을 할 때 사용합니다.

Link 컴포넌트는 내부적으로 브라우저의 History API를 사용하여 SPA의 페이지 전환을 최적화 합니다. 또한 프리페칭 기능을 지원하여 링크가 뷰포트에 나타나기 전에 해당 페이지의 데이터를 미리 불러오는 최적화 작업을 수행할 수 있습니다. 이는 사용자 경험을 개선할 수 있습니다. 또한 코드의 가독성 면에서도 Link 컴포넌트가 유리합니다.

 

반면에 useRouter 훅은 프로그래밍 방식으로 라우팅을 제어하기 때문에 사용자 상호작용에 따라 - 폼 제출 이후 다른 링크로 가야 하는 상황이라던가 - 유연적으로 대처할 수 있으며 쿼리 파라미터를 추가하여 동적인 라우팅을 가능하도록 만듭니다.)

 

 

 

 

 

 

페칭해 온 데이터를 쓸 수 있도록 이미지에는 onClick, useRouter 훅을 쓰고 제목에는 링크를 걸어 라우팅을 해준다.

이렇게 다른 주소로 넘어가게 되면 우리가 기존에 해놓은 movies/[id]폴더의 Page로 넘어가게 된다.(동적 라우팅 해놓은 상태니까)

 

(여기서! 항상 Link와 onClick으로 라우팅 해줄 때 주소가 위에서 언급한대로 파일 구조와 맞는지 확인하자…. 아무도 확인해 줄 수 없다.)


 

 

 

 

 

 

 

동적 메타데이터 갖는 법

그냥 데이터 페치해서 가져오면 됩니다 ㅎㅎ 심플

근데 이때, 다른 컴포넌트에서 사용한

export async function getMovie(id: string) {
  const response = await fetch(`${API_URL}/${id}`);
  return response.json();
}

 

 

 

 

이런 함수를 가져와서

import { Suspense } from "react";
import MovieInfo, { getMovie } from "../../../components/movie-info";
import MovieVideos from "../../../components/movie-videos";

interface IParams {
  params: { id: string };
}

export async function generateMetadata({ params: { id } }: IParams) {
  const movie = await getMovie(id);
  return {
    title: movie.title,
  };
}

export default async function MovieDetailPage({ params: { id } }: IParams) {
  return (
    <div>
      <Suspense fallback={<h1>Loading...</h1>}>
        <MovieInfo id={id} />
      </Suspense>
      <Suspense fallback={<h1>Loading...</h1>}>
        <MovieVideos id={id} />
      </Suspense>
    </div>
  );
}

 

 

 

 

이렇게 써버려도 된다.

엥??? 근데 메타데이터 얻을 때랑 무비 인포 얻을 때 똑같은 api 두번 호출해서 페치하잖아요. 이래도 되는건가요?

⇒ nextjs 최신버전은 상관없어요. 어차피 두번째로 페치할 때에는 캐시된 응답을 받게되니까요.

 

 

 

 

 

 

참고한 문서: https://velog.io/@ken1204/SPA에-대하여

https://en.wikipedia.org/wiki/Single-page_application

https://poiemaweb.com/js-spa

https://www.google.com/search?client=safari&rls=en&q=spa+next.js&ie=UTF-8&oe=UTF-8#ip=1

https://narup.tistory.com/235

https://parkgang.github.io/blog/2021/09/07/lets-properly-understand-and-use-the-ssg-of-nextjs/