Next - NextJS Study1

 

1. Next.js

1) Next.js 개념

  • Next.js는 오픈 소스 JS 프레임워크이다. React.js는 오픈 소스 JS 라이브러리이다.


  • Next.js는 React와 매우 유사하고 몇가지 메서드와 개념이 추가된다. 그래서 React.js의 확장판이다.


  • 개발 시, Next.js는 프레임워크에서 정해놓은 방법대로 개발을 해야 한다. 반면, React.js는 프로그래머가 무슨 도구를 쓰던간에 상관없다.


  • 또한, SSR 방식을 제공한다. CSR과 혼합해서 사용 가능하다. 기술적, 비즈니스적으로 매우 효율이 좋다.


  • SEO(검색 엔진)에 최적화되어 있다.



2) Next.js 프로젝트 생성 및 디렉토리 구조 설명

  • 터미널에서 Next.js 프로젝트를 생성하는 명령어** : npx create-next-app@latest .
    • npx는 create-next-app라는 node.js 라이브러리(보일러 플레이트 역할을 하는 라이브러리)로서 로컬에 설치하여 생성하지 않고 빌려서 사용하는 개념이다.
    • 또한, next.js의 버전이 계속 바뀌기 때문에, 새로운 next 앱을 생성할 때만 사용하기 때문에 로컬에 설치할 필요가 없다.
    • 해당 명령어에서 ‘.’이 중요하며, 터미널에서 입력 후 생성 마법사 과정을 진행하여 프로젝트 생성 마무리 짓기!!


  • 현재는, App Router보다는 Page Router를 사용할 예정이다. App Router는 서버 컴포넌트나 서스펜스 등등의 추가 개념이 필요하여 러닝커브가 높아서 추후에 공식문서로 학습하기!


  • 현재는 .js 파일로 Next.js의 페이지를 가리키는 컴포넌트 파일로서 생성한다.(나중에는 ts를 사용)


  • 페이지의 경로는 기본적으로 jsconfig.json 파일에서 @/pages, @/public 경로로 설정되어 있다.
{
  "compilerOptions": {
    "paths": {
      "@/*": ["./*"]
    }
  }
}


  • 중요** : 디렉토리 구성에서 pages 폴더 내부에 있는 .js 파일들은 각각 페이지 역할을 하는 컴포넌트를 정의하는 파일들이다.(Page Router 개념)


  • Next.js는 module css 방식을 이용하는 것을 권장한다.(/styles/Home.module.css의 방식처럼)



2. 페이지 라우터

1) Page Router 개념

  • 정적 경로 페이지 : post 디렉토리
    • post 디렉토리 내부의 index.js는 /post라는 경로를 의미한다. 물론, 사용자가 임의로 만든 post 디렉토리가 아니라 pages 디렉토리 바로 아래에 about.js가 있다면 /about라는 경로를 의미한다.


  • 동적 경로 페이지 1 : post 디렉토리 내부의 [id].js 파일
    • /post/1/post/2처럼 동적 경로를 정의할 수 있다.


  • 동적 경로 페이지 2 : country 디렉토리 내부의 […code].js 파일
    • 이러한 방식은 Catch All Routing이라고 불린다.
    • /country 뒤에 모든 동적 경로를 대응해준다.
      • /country/3/23423/asdf에 대응해준다.


  • 동적 경로 페이지 3 : country 디렉토리 내부의 [[…code]].js 파일
    • 이러한 방식은 해당 동적경로가 없어도 대응해준다. 404 페이지에 설정 가능!
    • 즉, ‘Optional’하다고 해서 Optional Catch All Routing이라고 부른다.


  • 이렇게 디렉토리 구조만 만들면, Next.js가 알아서 라우터가 정의한다.


  • _document.js, _app.js :
    • 모든 페이지나 모든 컴포넌트들에 공통적으로 적용되는 로직이나 데이터를 다루는 컴포넌트이다. _app.js는 모든 컴포넌트들의 루트 컴포넌트를 의미한다.



a. Page Router 라우팅 과정 및 _app.js 컴포넌트 개념**

  • 1) /about 경로를 요청하면, 첫째로, pages 디렉토리 내부에서 about.js라는 파일을 찾는다.


  • 2) 둘째로, 해당 about.js 파일에서 내부에서 React 컴포넌트를 내보낸다.(현재는 AboutPage라는 React 컴포넌트)
    • about.js
export default function AboutPage() {
  return <div>About Page</div>;
}


  • 3) 내보낸 React 컴포넌트를 _app.js 파일 내부의 Component Props로 전달하여 이렇게 렌더링되고 해당 페이지가 사용자에게 응답된다.
    • App.js
import "@/styles/globals.css";

export default function App({ Component, pageProps }) {
  return <Component {...pageProps} />;
}



b. _document.js 컴포넌트 개념

  • _document.js :
    • 모든 페이지에 적용되는 html 코드들이다.(폰트나 메타태그(ogTag), Character-Set, 서드파티스크립트 등의 설정과 그런 것들)
    • 즉, React.js에서의 index.html 역할이다. 껍데기 역할



2) Page Router 실습

  • useRouter를 이용하여 라우팅에 관한 정보를 얻어올 수 있는데 이러한 방식으로 ‘쿼리스트링’의 인자 값을 가져온다.


  • /pages/country/[code].js
import { useRouter } from "next/router";

export default function Country() {
  const router = useRouter();
  const { code } = router.query;

  return <div>Country {code}</div>;
}


a. Page Router 라우팅 과정**

  • 1) /about 경로를 요청하면, 첫째로, pages 디렉토리 내부에서 about.js라는 파일을 찾는다.


  • 2) 둘째로, 해당 about.js 파일에서 내부에서 React 컴포넌트를 내보낸다.(현재는 AboutPage라는 React 컴포넌트)
    • about.js
export default function AboutPage() {
  return <div>About Page</div>;
}


  • 3) 내보낸 React 컴포넌트를 _app.js 파일 내부의 Component Props로 전달하여 이렇게 렌더링되고 해당 페이지가 사용자에게 응답된다.
    • App.js
import "@/styles/globals.css";

export default function App({ Component, pageProps }) {
  return <Component {...pageProps} />;
}




3) Page 이동하기

  • 페이지를 이동하기 위해서 Link 컴포넌트를 이용하기! Next.js에서는 기본적으로 제공하는 해당 컴포넌트로서 CSR 방식으로 페이지 이동이 가능하다.


  • 페이지 이동을 위해 서버에서 요청하지 않고 자체적으로 컴포넌트를 교체하기 때문에 효율적이다.


  • 사용 방식 1 : 템플릿 리터럴 방식, 기본 문자열도 가능, 간단하면 이렇게 문자열로 이동시키기!!
    • <Link href={\/country/${code}`}>`


  • 사용 방식 2 : Url Object라는 객체 전달하기(Link 컴포넌트의 속성으로 href={} 속성에 해당 객체 넣기), 리터럴 객체 형식이다.
    • pathname: "/country/[code]", : 해당 페이지를 담당하는 컴포넌트가 작성된 파일의 이름을 넣기
    • query: { code: code },



b. Page 이동하기의 두번째 방식 : useRouter 컴포넌트 이용

  • 이벤트에 관하여 페이지가 이동하도록 하기 위해서는 이벤트 핸들러에 useRouter 컴포넌트 이용하기


  • React.js처럼 router.push() 이용하기! useRouter 컴포넌트에는 push말고도 back, replace, reload 등 여러 상황에 맞는 여러가지 메서드들이 있다.
    • useRouter에 관하여 공식문서를 참고하여 메서드 역할 알아보기!!



c. 실습 코드

  • index.js
import Link from "next/link";
import { useRouter } from "next/router";

export default function Home() {
  const code = "KOR";

  const router = useRouter();

  const onClickButton = () => {
    router.push({
      pathname: "/country/[code]",
      query: { code: code },
    });
  };

  return (
    <div>
      Home Page
      <div>
        <button onClick={onClickButton}>
          Search 페이지로 이동
        </button>
      </div>
      <div>
        <Link `={"/search"}>Search Page 이동</Link>
      </div>
      <div>
        <Link
          href={ {
            pathname: "/country/[code]",
            query: { code: code },
          } }
        >
          {code} 페이지로 이동
        </Link>
      </div>
    </div>
  );
}




4) 공통으로 사용하는 레이아웃 설정하기

  • React.js와 같은 방식으로서 Layout.js 컴포넌트에 공통 레이아웃을 설계!!


  • Layout 컴포넌트에서 'children'으로 페이지 역할을 하는 컴포넌트를 main 태그에 렌더링 시켜서 props 시킨다.


  • Next.js 앱의 페이지 전체에 스타일을 먹이려면 global.css에서 적용한다.



a. 실습 코드

  • Layout.js
import style from "./Layout.module.css";

export default function Layout({ children }) {
  return (
    <div>
      <header className={style.header}>NARAS 🌏</header>
      <main className={style.main}>{children}</main>
    </div>
  );
}


  • Layout.module.css
.header {
  position: fixed;
  top: 0px;
  left: 0px;
  right: 0px;
  height: 50px;
  background-color: white;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: bold;
  cursor: pointer;
}

.main {
  max-width: 700px;
  margin: 0 auto;
  padding: 80px 10px;
}


  • _app.js
import "@/styles/globals.css";
import Layout from "@/components/Layout";

export default function App({ Component, pageProps }) {
  return (
    <Layout>
      <Component {...pageProps} />
    </Layout>
  );
}


  • styles/globals.css
html,
body {
  margin: 0px;
  background-color: rgb(245, 245, 245);
}




5) 페이지별로 사용할 수 있는 레이아웃 설정하기

  • 페이지별로 사용할 수 있는 레이아웃을 설정하기 위해서 SubLayout이나 Component.Layout 설정


  • 중요** : JS 함수는 모두 객체이기 때문에 우리는 이전에 컴포넌트를 만들기 위해서 Search라는 함수를 만들었지만 사실은 객체로도 사용할 수 있다.
    • Search.Layout = SubLayout;
    • 정리하면, Search 컴포넌트의 ‘Layout’이라는 프로퍼티를 추가하고 그 값을 ‘SubLayout’ 컴포넌트에 저장한다라는 개념이다.


  • 추가적인 에러 발생 : index 페이지는 search, country 페이지와 달리 Layout의 초기값을 SubLayout처럼 설정하지 않았기 때문에 undefined이 반환되어 에러가 발생한다.
    • 해결 방법** : 그래서, 추가적으로 SubLayout에 EmptyLayout인 아무것도 없는 Layout을 반환하는 캄포넌트를 설정하여 undefined 에러를 해결한다.



a. 실습 코드 :

  • SubLayout.js
import style from "./SubLayout.module.css";

export default function SubLayout({ children }) {
  return (
    <div className="SubLayout">
      <div>{children}</div>
      <footer className={style.footer}>@winterlood</footer>
    </div>
  );
}


  • SubLayout.module.css
.footer {
  border-top: 1px solid rgb(230, 230, 230);
  padding-top: 20px;
  margin-top: 20px;
  text-align: center;
  color: gray;
  font-size: 14px;
}


  • /pages/country/[code].js
import SubLayout from "@/components/SubLayout";
import { useRouter } from "next/router";

export default function Country() {
  const router = useRouter();
  const { code } = router.query;

  return <div>Country {code}</div>;
}

Country.Layout = SubLayout;


  • /pages/search/index.js
import SubLayout from "@/components/SubLayout";

export default function Search() {
  return <div>Search Page</div>;
}

Search.Layout = SubLayout;


  • /pages/_app.js : 에러 수정 후
import "@/styles/globals.css";
import Layout from "@/components/Layout";

export default function App({ Component, pageProps }) {
  const EmptyLayout = ({ children }) => <>{children}</>;
  const SubLayout = Component.Layout || EmptyLayout;

  return (
    <Layout>
      <SubLayout>
        <Component {...pageProps} />
      </SubLayout>
    </Layout>
  );
}

  • /pages/_app.js : 에러 수정 전, SubLayout이 Component.Layout였다.
import "@/styles/globals.css";
import Layout from "@/components/Layout";

export default function App({ Component, pageProps }) {
  return (
    <Layout>
      <Component.Layout>
        <Component {...pageProps} />
      </Component.Layout>
    </Layout>
  );
}




3. SSR 렌더링

1) SSR에 Props 적용하기

a. getServerSideProps vs Component function

  • next.js에서 getServerSideProps 함수는 SSR 전용, 일반 컴포넌트 함수는 CSR 전용이라서 2가지를 혼용해서 사용할 수 있다.
    • next.js에서 보통 데이터를 만들어오는 곳은 ‘SSR’ 방식으로 사용하고, 화면에 보여주는 용도에서는 ‘CSR’ 방식으로 사용한다.


  • props** : 데이터는 props라는 프로퍼티로 설정되며 객체 타입이다. 또한, SSR로서 서버의 데이터를 넘겨받아 Home 컴포넌트로 넘겨준다.


  • 중요** : getServerSideProps는 서버 측에서만 실행되어 사용된다. 그래서 console.log에는 안보이고 터미널에서 볼 수 있다.
    • 즉, window 객체에는 접근할 수 없다.(ex) window.location는 브라우저에서만 접근이 가능하다.)
    • 추가적으로, 브라우저에서만 console.log를 보기 위해서는 useEffect()를 이용한다.(마운트 개념 사용)



b. getServerSideProps 실습 코드

  • /pages/index.js
import { useEffect } from "react";

export default function Home({ name }) {
  console.log("HOME");

  useEffect(() => {
    console.log("HOME MOUNT");
  }, []);

  return <div>{name}</div>;
}

export const getServerSideProps = async () => {
  // SSR을 위해 서버측에서 페이지 컴포넌트에게 전달할 데이터를 설정하는 함수

  return {
    props: {
      name: "KOREA",
    },
  };
};




2) SSR에 API 적용하기

a. useRouter vs context

  • 중요** : async-await 방식의 fetch API 데이터 받기!!, API fetch 결과에 대한 유효성 검사 방법!!


  • context** : context 객체는 서버측에서 API의 데이터를 사용할 때, 이용한다.
    • 서버 측에서 어떤 데이터를 fetch하기 위해서 불러온다면 context 객체를 사용한다.


  • useRouter** : useRouter 컴포넌트는 클라이언트측에서 API의 데이터를 사용할 때, 이용한다.
    • useRouter는 React의 Hook이라서, 브라우저에서 하이드레이션 과정이 이루어지고나서 동작할수 있게 된다. 즉, 클라이언트 측에서만 사용할 수 있다.


  • 하이드레이션 과정** : 브라우저가 React에서 작업된 JS 번들 코드와 HTML 코드들을 서로 연결하는 과정이다.



b. useRouter vs context 실습 코드

  • /pages/search/index.js
import { fetchSearchResults } from "@/api";
import SubLayout from "@/components/SubLayout";

export default function Search({ countries }) {
  return (
    <div>
      {countries.map((country) => (
        <div key={country.code}>{country.commonName}</div>
      ))}
    </div>
  );
}

Search.Layout = SubLayout;

export const getServerSideProps = async (context) => {
  // 1. 검색 결과 API 호출
  // 2. Props 리턴

  const { q } = context.query;

  let countries = [];
  
  // 3. API fetch 결과에 대한 유효성 검사 !!
  if (q) {
    countries = await fetchSearchResults(q);
  }

  return {
    props: {
      countries,
    },
  };
};


  • /pages/coutry/[code].js
import { fetchCountry } from "@/api";
import SubLayout from "@/components/SubLayout";
import { useRouter } from "next/router";

export default function Country({ country }) {
  const router = useRouter();
  const { code } = router.query;

  return (
    <div>
      {country.commonName} {country.officialName}
    </div>
  );
}

Country.Layout = SubLayout;

export const getServerSideProps = async (context) => {
  const { code } = context.params;

  let country = null;
  
  // 3. API fetch 결과에 대한 유효성 검사 !!
  if (code) {
    country = await fetchCountry(code);
  }

  return {
    props: {
      country,
    },
  };
};


  • api.js
    • 기존 Naras 프로젝트 api를 그대로 가져옴
import axios from "axios";

export async function fetchCountries() {
  try {
    const response = await axios.get(
      "https://naras-api.vercel.app/all"
    );
    return response.data;
  } catch (e) {
    return [];
  }
}

export async function fetchSearchResults(q) {
  try {
    const response = await axios.get(`
  https://naras-api.vercel.app/search?q=${q}
  `);

    return response.data;
  } catch (e) {
    return [];
  }
}

export async function fetchCountry(code) {
  try {
    const response = await axios.get(
      `https://naras-api.vercel.app/code/${code}`
    );
    return response.data;
  } catch (e) {
    return null;
  }
}




4. SSG 렌더링

1) SSG 렌더링 개념 정리

  • SSG 렌더링 : 정적 사이트 생성라고 불린다. 렌더링 전략 중 하나이다.


  • 생성 과정 : 서버 측에서 페이지를 빌드 타임에 한번만 렌더링 하는 것이다. 페이지를 언제 생성하는가가 SSR과 SSG의 차이점이다.
    • 배포할 때, 빌드를 하게 되는데 이렇게 빌드 타임에 미리 생성이 되는 것이 SSG 방식이다.


  • 장점 : 사용자의 요청이 들어오면, 응답 속도가 SSR 방식보다 매우 빠르다.


  • 단점 : 하지만, 언제 요청해도 똑같은 페이지만 응답하게 되어 최신 데이터를 반영할 수 없다.
    • 그래서, 페이지 내부 데이터가 변경되지 않는 페이지에 적절한 렌더링 전략이다.



2) SSG : getStaticProps

  • getStaticProps** :
    • getStaticProps는 정적으로 미리 만들어 놓을 페이지를 설정한다.


a. 빌드 시, console.log의 터미널 정리

  • 개발 모드** : SSR이던 SSG던 편의를 위해서, 해당 함수 내부에 있는 console.log는 터미널에서 여러 번 호출된다.


  • 빌드 모드** : 해당 함수 내부에 있는 console.log는 빌드 타임시, 터미널에서 1번 호출된다.


b. 실습 코드 :

  • /pages/index.js
import { fetchCountries } from "@/api";
import { useEffect } from "react";

export default function Home({ countries }) {
  return (
    <div>
      {countries.map((country) => (
        <div key={country.code}>{country.commonName}</div>
      ))}
    </div>
  );
}

export const getStaticProps = async () => {
  // API 호출 코드가 필요함

  const countries = await fetchCountries();
  console.log("countries 데이터 불러옴");

  return {
    props: {
      countries,
    },
  };
};




3) SSG : getStaticPaths

  • getStaticProps :
    • getServerSideProps와 비교하여 getStaticProps를 이용하는데, 미리 만들어 놓는 페이지를 설정한다.
    • 추가적으로 동적 경로를 설정하기 위해서는 getStaticPaths가 필요하다.


  • getStaticPaths :
    • 동적 경로에서는 어떤 페이지들을 미리 빌드타임에 생성할건지 미리 명시해야 한다. 이동시킬 페이지 설정!!


  • getStaticPaths 내용대로 빌드를 하면, getStaticPaths의 paths에서 params 설정대로 이동시킬 파일이 미리 생성된다.
    • ./next/server/pages : 빌드 관련 페이지들이 여기에 저장된다.
      • ABW.html, KOP.html으로 사전에 SSG 방식으로 생성


a. 빌드 시, 터미널 정리

  • 빌드 후 터미널의 하얀색 동그라미** : getStaticProps 함수를 설정하여 SSG 방식으로 설정된 경우


  • 빌드 후 터미널의 검은색 동그라미** : 어떤 컴포넌트에 getStaticProps라는 함수를 설정하지 않아 데이터를 불러오지 않고 진짜로 정적인 페이지일 경우


b. 실습 코드 :

  • /pages/country/[code].js
import { fetchCountry } from "@/api";
import SubLayout from "@/components/SubLayout";
import { useRouter } from "next/router";

export default function Country({ country }) {
  const router = useRouter();
  const { code } = router.query;

  if (router.isFallback) {
    return <div>Loading ...</div>;
  }

  if (!country) {
    return <div>존재하지 않는 국가입니다</div>;
  }

  return (
    <div>
      {country.commonName} {country.officialName}
    </div>
  );
}

Country.Layout = SubLayout;

export const getStaticPaths = async () => {
  return {
    paths: [
      { params: { code: "ABW" } },
      { params: { code: "KOR" } },
    ],
    fallback: false,
  };
};

export const getStaticProps = async (context) => {
  const { code } = context.params;

  let country = null;
  if (code) {
    country = await fetchCountry(code);
  }

  return {
    props: {
      country,
    },
  };
};




4) SSG : fallback 옵션

a. getStaticPaths 함수의 fallback 옵션 개념 :

  • false** :
    • 빌드 타임에 미리 만들어 놓지 않은 페이지들은 404 에러를 보낸다.


  • "blocking"** :
    • 존재하지 않는 페이지도 서버가 실시간으로 생성하여 해당 페이지를 server에 저장한다. 추후 해당 페이지 요청에서는 server에 저장된 페이지들을 불러오기 때문에 매우 빠르다.


  • true** :
    • fallback 옵션이 "blocking"일 때, 서버가 페이지를 생성하는 시간동안 만큼은 추가적으로 페이지를 응답해주지 않는다. 따라서, 브라우저는 대기할 수 밖에 없다. 결국, 브라우저는 로딩이 발생한다.
    • fallback 옵션을 true로 설정하면, props가 없는 버전의 페이지(데이터가 없는 상태의 페이지)를 아주 빠르게 반환한다. 서버의 백그라운드에서 props의 계산이 완료되면, 뒤늦게 props(데이터가 있는 상태의 페이지)를 보내준다.
    • 빌드 타임에 생성해놓지 않은 페이지도 제공할 수 있으면서, 사용자에게 로딩 대신에 데이터가 없는 페이지를 먼저 보여줄 수 있는 장점이 있다.



b. fallback 개념 정리

  • fallback 상태 : 데이터가 없는 상태의 페이지


  • isFallback : fallback 상태일 때, 화면 구성 가능



c. 실습 코드

  • /pages/country/[code].js
    • ‘존재하지 않는 국가입니다’를 먼저 응답되고 그 다음 국가명을 응답해준다.
    • 결과적으로 뒤늦게 데이터를 전달해준다.
    • 데이터를 불러오는 과정이 오래걸릴 것 같다면, 일단 사용자에게 페이지를 빨리 보여주는 방식이다.
    • 한 번 생성된 페이지는 서버에 저장된다. 추후에 해당 페이지를 요청하면 이미 생성된 페이지가 SSG 방식으로 동작된다.
import { fetchCountry } from "@/api";
import SubLayout from "@/components/SubLayout";
import { useRouter } from "next/router";

export default function Country({ country }) {
  const router = useRouter();
  const { code } = router.query;

  if (router.isFallback) {
    return <div>Loading ...</div>;
  }

  if (!country) {
    return <div>존재하지 않는 국가입니다</div>;
  }

  return (
    <div>
      {country.commonName} {country.officialName}
    </div>
  );
}

Country.Layout = SubLayout;

export const getStaticPaths = async () => {
  return {
    paths: [
      { params: { code: "ABW" } },
      { params: { code: "KOR" } },
    ],
    fallback: true,
  };
};

export const getStaticProps = async (context) => {
  const { code } = context.params;

  let country = null;
  if (code) {
    country = await fetchCountry(code);
  }

  return {
    props: {
      country,
    },
  };
};




5) SSG 적용하기 4 : 쿼리스트링 처리, CSR

a. URL parameter vs 쿼리스트링 설정

  • URL parameter일 경우, KOR, ITA처럼 어느 정도 제한할 수 있다.


  • 하지만, 쿼리스트링인 경우, 중구난방일 수 도 있고 사용자가 직접 아무렇게나 입력할 수도 있어서
    • 결국, 다이나믹한 SSG로 구현하기 어렵다. 구현할 수 있지만 어렵다.**


  • 결론** : 그래서, 쿼리스트링처럼 서버 측에서 굳이 렌더링할 필요가 없는 데이터들은 SSR 방식의 코드 부분을 전부 지우고, SSR 방식보다는 CSR 방식을 이용한다.(React.js처럼)



b. Next.js에서 getStaticProps, getServerSideProps가 없는 경우처럼 CSR 방식

  • getStaticProps, getServerSideProps가 없는 경우, 기본적으로는 SSG 방식으로 동작을 한다.


  • country라는 데이터는 현재, Client Side에서 API 호출을 하기 때문에 일단은 Search Page에 빈 div 태그만 렌더링되었다가 이 검색 결과 데이터는 이 API가 완료되는 시점에 CSR에서 추가로 렌더링이 된다.



c. 테스트 및 결론

  • 테스트 :
    • 실제로 페이지를 새로고침 해보면, 빈페이지였다가, Search 페이지에서 데이터가 렌더링되는 모습을 확인할 수 있다.


  • 결론** :
    • 검색 결과처럼, 서버 측에서 굳이 렌더링할 필요가 없는 데이터들은 이런 데이터는 useEffect()를 통해서 Client Side에서 직접 불러오도록 설정할 수도 있다.



d. 실습코드

  • /pages/search/index.js
import { fetchSearchResults } from "@/api";
import SubLayout from "@/components/SubLayout";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";

export default function Search({ countries }) {
  const router = useRouter();
  const { q } = router.query;

  const [countries, setCountries] = useState([]);

  const setData = async () => {
    const data = await fetchSearchResults(q);
    setCountries(data);
  };

  useEffect(() => {
    if (q) {
      setData();
    }
  }, [q]);

  return (
    <div>
      {countries.map((country) => (
        <div key={country.code}>{country.commonName}</div>
      ))}
    </div>
  );
}

Search.Layout = SubLayout;




5. ISR 렌더링

1) 증분 정적 재생성 개념

  • 일정 시간을 주기로 정적 페이지를 다시 생성하는 기술


  • SSG 방식으로 동작하는 정적 페이지를 일정 시간이 지나면, 서버 측에서 페이지를 다시 생성하도록 하여 최신 데이터를 페이지에 반영하도록 한다.
    • 딱 그 일정 시간에 최신 데이터를 포함한 페이지가 다시 생성이 되는 것이 아니라, 그 일정 시간 이후에 생성이 되고 추가적인 요청 사항에 있을 때, 최신 데이터가 반영된 페이지를 사용자에게 응답해준다.



2) 실습 코드

  • getStaticProps 메서드이 반환하는 값 중 props 말고도 revalidate를 프로퍼티로 추가하여 일정 시간을 설정한다.
    • revalidate : 3 : 일정 시간은 3초를 의미한다.
    • 이러한 일정 시간 이후에 getStaticProps를 다시 실행하여 최신 데이터가 반영된 페이지를 생성한다.


  • /pages/country/[code].js
import { fetchCountry } from "@/api";
import SubLayout from "@/components/SubLayout";
import { useRouter } from "next/router";

export default function Country({ country }) {
  const router = useRouter();
  const { code } = router.query;

  if (router.isFallback) {
    return <div>Loading ...</div>;
  }

  if (!country) {
    return <div>존재하지 않는 국가입니다</div>;
  }

  return (
    <div>
      {country.commonName} {country.officialName}
    </div>
  );
}

Country.Layout = SubLayout;

export const getStaticPaths = async () => {
  return {
    paths: [
      { params: { code: "ABW" } },
      { params: { code: "KOR" } },
    ],
    fallback: true,
  };
};

export const getStaticProps = async (context) => {
  const { code } = context.params;
  console.log(`${code} 페이지 생성!`);

  let country = null;
  if (code) {
    country = await fetchCountry(code);
  }

  return {
    props: {
      country,
    },
    revalidate: 3,
  };
};




3) 결론**

  • ISR 렌더링 방식을 포함한 앞서 배운 여러가지 렌더링 방식은 각 페이지마다 각각 다르게 설정할 수 있어서 페이지마다 특성에 걸맞게 설정할 수 있어야 한다.


  • 중요** : 정적 페이지만 필요한 경우(SSG), 최신 데이터를 필요한 경우(ISR), 실시간으로 데이터가 필요한 경우(SSR), SSR이 굳이 필요 없어서 CSR 방식로만 동작 가능한 경우(CSR)에 걸맞게 렌더링 전략을 이용하자!!



6. Next.js를 이용한 Naras 프로젝트

1) Next.js의 새 프로젝트 생성 시, 주의점**

  • 문제점 :
    • 현재, 최신 Next.js 버전은 14라서 node.js도 18.17.0 버전으로 올려야 한다. 하지만, 현재 내컴퓨터의 node.js 버전은 18.15.0이다.
    • 그래서, 위의 명령어로 프로젝트를 생성하면, Next.js 프로젝트가 동작하지 않는다.
    • 그냥 14버전 사용하면 문제는 없었다.


  • 해결 방법** :
    • 강의 버전인 npx create-next-app@13.4.12 .인 명령어로 프로젝트를 생성한다.
    • 이렇게 생성하면 Next.js의 버전은 13.4.12 버전이다.
    • 해당 생성 명령어에서 ‘.’가 매우 중요하다. 이게 있어야 프로젝트에 .next 폴더가 생기더라.


  • Next.js 프로젝트 생성의 마법사 과정 :
>> npx create-next-app@13.4.12 .
✔ Would you like to use TypeScript? … 'No' / Yes
✔ Would you like to use ESLint? … No / 'Yes'
✔ Would you like to use Tailwind CSS? … 'No' / Yes
✔ Would you like to use `src/` directory? … 'No' / Yes
✔ Would you like to use App Router? (recommended) … 'No' / Yes
✔ Would you like to customize the default import alias? … 'No' / Yes
Creating a new Next.js app in /Users/jh/temp/reactStudy/basicNextJSPrj/section12.


  • Next.js 프로젝트 실행 :
    • npm run dev


  • Next.js 프로젝트 빌드 후 실행 :
    • npm run build
    • npm run start



2) 공통 레이아웃, 이전에 배운 렌더링 방식을 Naras 프로젝트에 적용

  • Next.js에서 페이지 경로나 파일 import는 @/* 타입으로 정해진다.


a. 실습 코드


a) _app.js
import "@/styles/globals.css";
import Layout from '@/components/Layout'

// ** App 컴포넌트에 공통 레이아웃 설정하기!!
export default function App({ Component, pageProps }) {
  
  // 빈 컴포넌트 생성 & 유효성 검사!
  const EmptyLayout = ( { children} ) => <>{children}</>;
  const SubLayout = Component.Layout || EmptyLayout;

  return (
  <Layout>
    <SubLayout>
      <Component {...pageProps} />
    </SubLayout>
  </Layout>
  )
}



b) Layout.js
  • children라는 props로 임의의 컴포넌트 정보를 받는다.


  • Next.js는 useRouter 메서드가 기본적으로 깔려 있다.
import { useRouter } from "next/router";
import style from "./Layout.module.css";

export default function Layout( { children } ) {

    // Next.js는 useRouter 메서드가 기본적으로 깔려 있다.
    // 그래서 바로 해당 메서드 불러오기!! 
    // React는 추가로 npm 설치 필요!
    const router = useRouter();

    const onClickHeader = () => {
        router.push("/");
    };

    return (
        <div>
            <header
                onClick={onClickHeader}
                className={style.header}
            >
                Naras 🌏
            </header>
            <main className={style.main}>{children}</main>
        </div>
    );
}



c) SubLayout.js
  • children라는 props로 임의의 컴포넌트 정보를 받는다.
import style from "./SubLayout.module.css";

export default function SubLayout( { children } ) {
    return (
        <div className="SubLayout">
            <div>
                {children}
            </div>
            <footer className={style.footer}>
                @jaeheonlood
            </footer>
        </div>
    );
}



d) index.js
  • 상위에 공통 스타일인 Layout과 SubLayout이 있었어서 해당 스타일을 먹이려고 여기서는 최상위 태그를 빈 태그로 설정한다.


  • SSG 방식인 정적 페이지를 불러오는 getStaticProps 메서드에서도 async-await가 중요하다!!
import { fetchCountries } from '@/api'
import CountryList from '@/components/CountryList'
import Searchbar from '@/components/Searchbar'
import { useEffect } from 'react'

export default function Home( {countries} ) {
  
  // 상위에 공통 스타일인 Layout과 SubLayout이 있었어서 해당 스타일을 먹이려고 여기서는 최상위 태그를 빈 태그로 설정한다.
  return (
    <>
      <Searchbar />
      <CountryList countries={countries} />
    </>
  )
}

// getStaticProps에도 async-await가 중요하다!!
export const getStaticProps = async () => {
  const countries = await fetchCountries();
  console.log("countries 데이터 불러옴");

  return {
    props: {
      countries,
    },
  };
};



e) /pages/search/index.js
  • Search 컴포넌트는 서버측 데이터를 쓰지 않아도 되어서 SSR 코드를 제거하고 CSR로 동작시킴.


  • 유효성 검사 중요!


  • 중요 1** : JS에서 함수는 객체이므로 프로퍼티로 사용하여 Search.Layout = SubLayout;라는 문법을 적용할 수 있다.
    • 이러한 방식으로 다른 컴프넌트로도 바꿔낄 수 있다.


  • 중요 2** : API가 원래도 async-await 프로미스 방식으로 설계되지만 사용할 때도 async-await 방식으로 받기!
import { fetchSearchResults } from "@/api";
import SubLayout from "@/components/SubLayout";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";

// ** Search는 서버측 데이터를 쓰지 않아도 되어서 SSR 코드를 제거하고 CSR로 동작시킴.
export default function Search() {
    const router = useRouter();
    const {q} = router.query;

    const [countries, setCountries] = useState([]);

    // 쿼리스트링 검색이라서 쿼리스트링을 API 인자에 넣어주기!!
    // API가 원래도 async-await 프로미스 방식으로 설계되지만 사용할 때도 async-await 방식으로 넘겨받기 
    const setData = async () => {
        const data = await fetchSearchResults(q);
        setCountries(data);
    }

    // 유효성 검사 중요!
    useEffect(()=>{
        if (q) {
            setData();
        }
    },[q]);

    return (
        <div>
            { countries.map((country) => (
                <div key={country.code}> {country.commonName} </div>
            ))}
        </div>
    );
}


// ** JS에서 함수는 객체이므로 프로퍼티로 사용하여 다음 문법을 적용할 수 있다. 
// ** 다른 컴프넌트로도 바꿔 낄 수 있다.
Search.Layout = SubLayout;



f) /pages/country/[code].js
  • Fallback 상태는 데이터를 아직 안불러온 상태이므로 Loading 상태로 대기를 걸어둔다!!, 유효성 검사 중요!!


  • getStaticProps 메서드에서 context 객체 중요!! 서버에서 넘겨주는 객체를 context로 받는다.
    • return 즉 반환되는 것이 객체라서 return이 {}의 형식이다.


  • getStaticPaths 메서드에서는 미리 생성할 페이지를 설정하는데 return에서 대괄호를 써야 해서 주의해야 한다.
    • 배열 안에 객체가 있는 형태이므로(마치 리스트에 객체가 있다.)


  • JS에서 함수는 객체이므로 프로퍼티로 사용하여 다음 문법을 적용할 수 있다.
    • 이러한 방식으로 다른 컴프넌트로도 바꿔 낄 수 있다.
import { fetchCountry } from "@/api";
import SubLayout from "@/components/SubLayout";
import { useRouter } from "next/router";
import { useReducer } from "react";

export default function Country( {country} ) {

    const router = useRouter();
    const { code } = router.query;  // 구조분해할당으로 받아도 된다!

    // ** Fallback 상태는 데이터를 아직 안불러온 상태이므로 Loading 상태로 대기를 걸어둔다!!
    if(router.isFallback){
        return <div>Loading....</div>;
    }

    // 유효성 검사 중요!!
    if(!country){
        return <div>존재하지 않는 페이지이빈다.</div>;
    }

    return (
        <div>
            {country.commonName} {country.officialName}
        </div>
    );
}

Country.Layout = SubLayout;
  
export const getStaticPaths = async () => {
    return {
        // 대괄호 주의!!(배열 안에 객체가 있는 형태이므로 마치 리스트마냥)
        paths : [
            { params: { code: "ABW" } },
            { params: { code: "KOR" } },
        ],
        fallback: true,
    };
};

// context 중요!! 서버에서 넘겨주는 객체를 context로 받는다.
export const getStaticProps = async ( context ) => {
    const {code} = context.params;
    console.log(`${code} 페이지 생성!`);
    
    let country = null;

    // 유효성 검사 중요!!
    if(code) {
        country = await fetchCountry(code);
    }

    // return 즉 반환되는 것이 객체라서 return이 {}의 형식이다.
    return {
        props : {
            country,
        },
         revalidate : 3,
    };
};



3) 홈 화면 구현 : Next.js

  • CountryItem.js
    • useNavigate는 React.js에서만 제공하기 때문에 Next.js는 제공하지 않아서 에러가 발생한다.
    • 따라서, useNavigate 메서드를 useRouter 메서드로 변경!!
import style from "./CountryItem.module.css";
import { useRouter } from "next/router";

export default function CountryItem({
  code,
  commonName,
  flagEmoji,
  flagImg,
  population,
  region,
  capital,
}) {
  const router = useRouter();

  const onClickItem = () => {
    router.push(`/country/${code}`);
  };

  return (
    <div onClick={onClickItem} className={style.container}>
      <img className={style.flag_img} src={flagImg} />
      <div className={style.content}>
        <div className={style.name}>
          {flagEmoji} {commonName}
        </div>
        <div>지역 : {region}</div>
        <div>수도 : {capital.join(", ")}</div>
        <div>인구 : {population}</div>
      </div>
    </div>
  );
}




4) 이미지 최적화하는 방법

  • 이미지 최적화를 위해서 Next.js가 제공해주는 Image 컴포넌트 이용하기!


a. 실습 코드

  • CountryItem.js
    • 이미지 최적화를 위해서 기존 CSS인 object-fit를 제외하고 Image 컴포넌트를 이용하는데 그 중 fill 속성을 이용하여 최적화 진행!
import Image from "next/image";
import style from "./CountryItem.module.css";
import { useRouter } from "next/router";

export default function CountryItem({
  code,
  commonName,
  flagEmoji,
  flagImg,
  population,
  region,
  capital,
}) {
  const router = useRouter();

  const onClickItem = () => {
    router.push(`/country/${code}`);
  };

  return (
    <div onClick={onClickItem} className={style.container}>
      <div className={style.flag_img}>
        <Image src={flagImg} fill />
      </div>
      <div className={style.content}>
        <div className={style.name}>
          {flagEmoji} {commonName}
        </div>
        <div>지역 : {region}</div>
        <div>수도 : {capital.join(", ")}</div>
        <div>인구 : {population}</div>
      </div>
    </div>
  );
}


  • CountryItem.module.css : 초기 Image의 CSS
.flag_img {
  width: 100%;
  height: 150px;
  object-fit: cover;
}


  • CountryItem.module.css : 수정 후 Image의 CSS
.flag_img {
  width: 100%;
  height: 150px;

  position: relative;
}



5) Search, Country 페이지 구현

  • onChange 이벤트 핸들러에 변수인 value={search || ''}에 초기 값을 걸어줘야 한다.


a. /components/Searchbar.js : 기존 React 프로젝트의 컴포넌트와 코드는 같다.

import { useEffect, useState } from "react";
import style from "./Searchbar.module.css";
import { useRouter } from "next/router";

export default function Searchbar({ q }) {
  const [search, setSearch] = useState("");
  const router = useRouter();

  useEffect(() => {
    setSearch(q);
  }, [q]);

  const onChangeSearch = (e) => {
    setSearch(e.target.value);
  };

  const onKeyDown = (e) => {
    if (e.keyCode === 13) {
      onClickSearch();
    }
  };

  const onClickSearch = () => {
    if (search !== "") {
      router.push(`/search?q=${search}`);
    }
  };

  return (
    <div className={style.container}>
      <input
        value={search || ''}
        onKeyDown={onKeyDown}
        onChange={onChangeSearch}
        placeholder="검색어를 입력하세요..."
      />
      <button onClick={onClickSearch}>검색</button>
    </div>
  );
}




b. /pages/search/index.js

import { fetchSearchResults } from "@/api";
import CountryList from "@/components/CountryList";
import Searchbar from "@/components/Searchbar";
import SubLayout from "@/components/SubLayout";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";

export default function Search() {
  const router = useRouter();
  const { q } = router.query;

  const [countries, setCountries] = useState([]);

  const setData = async () => {
    const data = await fetchSearchResults(q);
    setCountries(data);
  };

  useEffect(() => {
    if (q) {
      setData();
    }
  }, [q]);

  return (
    <>
      <Searchbar q={q} />
      <CountryList countries={countries} />
    </>
  );
}

Search.Layout = SubLayout;




c. /pages/country/[code].js

import { fetchCountry } from "@/api";
import SubLayout from "@/components/SubLayout";
import { useRouter } from "next/router";
import style from "./[code].module.css";
import Image from "next/image";

export default function Country({ country }) {
  const router = useRouter();
  const { code } = router.query;

  if (router.isFallback) {
    return <div>Loading ...</div>;
  }

  if (!country) {
    return <div>존재하지 않는 국가입니다</div>;
  }

  return (
    <div className={style.container}>
      <div className={style.header}>
        <div className={style.commonName}>
          {country.flagEmoji}&nbsp;{country.commonName}
        </div>
        <div className={style.officialName}>
          {country.officialName}
        </div>
      </div>

      <div className={style.flag_img}>
        <Image src={country.flagImg} fill />
      </div>

      <div className={style.body}>
        <div>
          <b>코드 :</b>&nbsp;{country.code}
        </div>
        <div>
          <b>수도 :</b>&nbsp;{country.capital.join(", ")}
        </div>
        <div>
          <b>지역 :</b>&nbsp;{country.region}
        </div>
        <div>
          <b>지도 :</b>&nbsp;
          <a target="_blank" href={country.googleMapURL}>
            {country.googleMapURL}
          </a>
        </div>
      </div>
    </div>
  );
}

Country.Layout = SubLayout;

export const getStaticPaths = async () => {
  return {
    paths: [
      { params: { code: "ABW" } },
      { params: { code: "KOR" } },
    ],
    fallback: true,
  };
};

export const getStaticProps = async (context) => {
  const { code } = context.params;
  console.log(`${code} 페이지 생성!`);

  let country = null;
  if (code) {
    country = await fetchCountry(code);
  }

  return {
    props: {
      country,
    },
    revalidate: 3,
  };
};




6) 메타 데이터 설정

  • Next.js는 페이지 별로 메타 데이터를 설정할 수 있어서 ogTag도 페이지별로 설정할 수 있다.


  • 기존 React.js는 SPA라서 페이지별로 메타 데이터를 설정할 수 없어서 항상 ogTag가 같다.(카톡에 URL 복사하여 공유 시, 썸네일이 항상 같다.)



a. 실습 코드

  • index.js
    • 기본 앱의 ogTag를 설정
import { fetchCountries } from "@/api";
import CountryList from "@/components/CountryList";
import Searchbar from "@/components/Searchbar";
import Head from "next/head";

export default function Home({ countries }) {
  return (
    <>
      <Head>
        <title>NARAS</title>
        <meta
          property="og:image"
          content="/thumbnail.png"
        />
        <meta property="og:title" content="NARAS" />
        <meta
          property="og:description"
          content="전 세계 국가들의 정보를 확인해보세요"
        />
      </Head>
      <Searchbar />
      <CountryList countries={countries} />
    </>
  );
}

export const getStaticProps = async () => {
  // API 호출 코드가 필요함

  const countries = await fetchCountries();
  console.log("countries 데이터 불러옴");

  return {
    props: {
      countries,
    },
  };
};



  • /pages/search/index.js
    • 페이지별로 ogTag를 생성할 수 있다.
    • 해당 앱의 search 페이지 ogTag 설정
import { fetchSearchResults } from "@/api";
import CountryList from "@/components/CountryList";
import Searchbar from "@/components/Searchbar";
import SubLayout from "@/components/SubLayout";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import Head from "next/head";

export default function Search() {
  const router = useRouter();
  const { q } = router.query;

  const [countries, setCountries] = useState([]);

  const setData = async () => {
    const data = await fetchSearchResults(q);
    setCountries(data);
  };

  useEffect(() => {
    if (q) {
      setData();
    }
  }, [q]);

  return (
    <>
      <Head>
        <title>NARAS 검색 결과</title>
        <meta
          property="og:image"
          content="/thumbnail.png"
        />
        <meta
          property="og:title"
          content="NARAS 검색 결과"
        />
        <meta
          property="og:description"
          content="전 세계 국가들의 정보를 확인해보세요"
        />
      </Head>
      <Searchbar q={q} />
      <CountryList countries={countries} />
    </>
  );
}

Search.Layout = SubLayout;



  • /pages/country/[code].js
    • API 상태에 따라 ogTag 생성**
    • 1) Fallback 상태(Props 데이터가 없는 상태)에서 해당 앱의 ogTag 생성
    • 2) 데이터가 있는 상태에서 ogTag 생성
import { fetchCountry } from "@/api";
import SubLayout from "@/components/SubLayout";
import { useRouter } from "next/router";
import style from "./[code].module.css";
import Image from "next/image";
import Head from "next/head";

export default function Country({ country }) {
  const router = useRouter();
  const { code } = router.query;

  if (router.isFallback) {
    return (
      <>
        <Head>
          <title>NARAS</title>
          <meta
            property="og:image"
            content="/thumbnail.png"
          />
          <meta property="og:title" content="NARAS" />
          <meta
            property="og:description"
            content="전 세계 국가들의 정보를 확인해보세요"
          />
        </Head>
        <div>Loading ...</div>
      </>
    );
  }

  if (!country) {
    return <div>존재하지 않는 국가입니다</div>;
  }

  return (
    <>
      <Head>
        <title>
          {country.commonName} 국가 정보 조회 | NARAS
        </title>
        <meta
          property="og:image"
          content={country.flagImg}
        />
        <meta
          property="og:title"
          content={`${country.commonName} 국가 정보 조회 | NARAS`}
        />
        <meta
          property="og:description"
          content={`${country.commonName} 국가의 자세한 정보입니다`}
        />
      </Head>
      <div className={style.container}>
        <div className={style.header}>
          <div className={style.commonName}>
            {country.flagEmoji}&nbsp;{country.commonName}
          </div>
          <div className={style.officialName}>
            {country.officialName}
          </div>
        </div>

        <div className={style.flag_img}>
          <Image src={country.flagImg} fill />
        </div>

        <div className={style.body}>
          <div>
            <b>코드 :</b>&nbsp;{country.code}
          </div>
          <div>
            <b>수도 :</b>&nbsp;{country.capital.join(", ")}
          </div>
          <div>
            <b>지역 :</b>&nbsp;{country.region}
          </div>
          <div>
            <b>지도 :</b>&nbsp;
            <a target="_blank" href={country.googleMapURL}>
              {country.googleMapURL}
            </a>
          </div>
        </div>
      </div>
    </>
  );
}

Country.Layout = SubLayout;

export const getStaticPaths = async () => {
  return {
    paths: [
      { params: { code: "ABW" } },
      { params: { code: "KOR" } },
    ],
    fallback: true,
  };
};

export const getStaticProps = async (context) => {
  const { code } = context.params;

  let country = null;
  if (code) {
    country = await fetchCountry(code);
  }

  return {
    props: {
      country,
    },
    revalidate: 3,
  };
};




7) 배포하기

  • 배포 명령어 : vercel deploy
    • Next.js 프로젝트의 터미널에서 위의 명령어로 배포하기