[React] Next.js! CRA와 뭐가 다른데?

MINEW·2023년 2월 19일
0

What's Next.js ?

  1. Next.js
    리액트는 기본적으로 SPA이고, CSR(클라이언트 사이드 렌더링)로 동작합니다.
    클라이언트 사이드 렌더링은 번들된 자바스크립트 파일 하나를 받는데, bundle.js 파일은 리액트 코드가 자바스크립트 코드로 변환되어 들어가있는 자바스크립트 파일입니다.
    브라우저가 이 자바스크립트 코드를 실행해서 DOM을 그리게 되는데, 그러다 보니 서버에서 내려받는 HTML은 비어있는 상태가 됩니다.
    구글을 제외한 다른 검색 엔진에서는 이 자바스크립트 코드를 실행하지 않고 HTML만 보고 검색 엔진 알고리즘을 돌리기 때문에 검색 엔진 최적화(SEO)에 불리합니다.
    이를 해결하기 위해 첫 페이지에는 SSR 즉, 완성된 HTML을 받아야겠다는 생각을 하게 되었고 효율적으로 개발하기 위해서 Next.js라는 프레임워크가 인기를 얻게 되었습니다.

  2. 설치 방법

// JS
npx create-next-app [프로젝트명]

// TS
npx create-next-app [프로젝트명] --typescript

디렉토리 구조

  1. src 디렉토리가 존재하지 않습니다.
    - CRA에서 src 디렉토리에 넣었던 하위 디렉토리들을 루트에 바로 생성하면 됩니다.

  2. 루트에 생성하는 디렉토리
    - routes + pages -> pages: [파일명].tsx
    - assets -> public: [파일명].svg 혹은 .jpg 등등 가능
    - styles: [파일명].module.css
    - apis: [파일명].ts
    - components: [파일명].tsx
    - types: [파일명].d.ts
    - hooks: [파일명].ts
    - utils: [파일명].ts
    - store: [파일명].ts
    - constants: [파일명].ts

pages 디렉토리 구조

  1. url 페이지 디렉토리
    1) http://localhost:3000 (pages 디렉토리 -> index.tsx)
    2) http://localhost:3000/blog (pages 디렉토리 -> blog 폴더 -> index.tsx)
    3) http://localhost:3000/blog/first (pages 디렉토리 -> blog 폴더 -> first.tsx)
    4) http://localhost:3000/숫자,문자,기호 (pages 디렉토리 -> [id].tsx)
    5) http://localhost:3000/blog/숫자,문자,기호 (pages 디렉토리 -> blog 폴더 -> [id].tsx)
    => 반드시 index 여야 작동합니다.
    => id 대신에 원하는 식별자 넣어도 됩니다 (동적 페이지).

  2. 참고: api 디렉토리
    - pages 디렉토리의 api 폴더는 api 통신을 위한 파일들을 모아놓은 곳 입니다.
    - pages 하위의 다른 폴더는 라우팅이 가능하지만, api 폴더는 라우팅이 불가능합니다 (url로 접근이 불가능 합니다).
    - 삭제하고 루트에 apis 디렉토리 생성 후 사용하는 것을 추천합니다.

  3. _app.tsx (CRA src 디렉토리의 App.tsx 와 같습니다)

// pages/_app.tsx
import "../styles/globals.css";
import type { AppProps } from "next/app";
import { Header } from "../components"; // 1번) 모든 페이지에 공통 컴포넌트르 넣고 싶을 때는

function MyApp({ Component, pageProps }: AppProps) {
  return ( // 2번) 다음과 같이 사용한다.
    <>
      <Header />
      <Component {...pageProps} />
    </>
  );
}

export default MyApp;

public 디렉토리 구조

  1. assets 디렉토리와 같은 역할
    - public 디렉토리의 하위 폴더는 images, fonts 등을 사용하면 됩니다. (assets와 같은 방식)

  2. 기본 예시

// pages/index.tsx
import Image from "next/image"; // 1번) img 태그를 바로 사용하는 것이 불가능하다. Image를 가져온 뒤,
import vercel from "../public/images/vercel.svg";

function Home() {
  return (
    <div>
      <Image src={vercel} alt="바셀이미지" /> // 2번) 사용해야만 한다. (충격..!)
      <p>이곳은 메인 페이지입니다.</p>
    </div>
  );
}

export default Home;

styles 디렉토리 구조

  1. css 파일들 디렉토리
    - globals.css 파일은 공통 css 파일입니다.
    - _app.tsx 를 제외한 모든 컴포넌트, 페이지 파일에 css 파일을 import 할 때는 -> 무조건 [파일명].module.css 로 설정해야 합니다.
    - ex) pages 디렉토리의 index.tsx 페이지의 Home.module.css 파일.

getStaticProps, getStaticPaths, getServerSideProps

  1. pre-render: 미리 HTML을 만드는 방식
    - getStaticProps: 고정된 페이지를 렌더링. 수정 불가능. user의 액션 적용 X.
    - getStaticPaths: 동적 라우팅이 필요할 때 일반적으로 사용.
    - getServerSideProps: 변경이 잦은 페이지를 렌더링. 성능 이슈 때문에 반드시 필요할 때만 사용.

  2. getStaticProps 기본 예시

// pages/blog/first.tsx
import { productsApi } from "../../apis/goodsApi";
import { ProductGuard } from "../../types/type";

function First({ productList }: { productList: ProductGuard[] }) {
  console.log(productList); // 3번) 20개의 상품리스트가 잘 들어온다.

  const products = productList.map((product, index) => (
    <div key={index}>
      {product.title}
    </div>
  ));

  return (
    <div>
      First 페이지
      <br />
      {products}
    </div>
  );
}

export async function getStaticProps() { // 1번) 여기에 async 붙여주기. 함수명은 반드시 getStaticProps 로 작성해야만 작동한다.
  // 2번) API 호출 등의 코드를 작성하기
  const productList = await productsApi(); // useEffect(() => {}, []) 대신 사용?

  return {
    props: {
      productList,
    },
  };
}

export default First;
  1. getStaticPaths 기본 예시
    1) pages/[id].tsx
// pages/[id].tsx
import { useRouter } from "next/router";
import { singleProductApi } from "../apis/goodsApi";
import { ProductGuard } from "../types/type";
import { NotFound } from "../components";

function HomeId({ singleProduct }: { singleProduct: ProductGuard }) {
  const router = useRouter();

  if (router.isFallback) { // 1번) fallback = true 일 때, 로딩시 보여주는 페이지.
    return <div>Loading...</div>;
  }

  if (!singleProduct) return <NotFound />; // 2번) 없어도 에러 발생 X. 404 페이지 대신 보여주고 싶은 컴포넌트 넣으면 된다.
  return (
    <div>
      <p>HomeId 페이지</p>
      <p>HomeId: { router.query.id }</p>
      <p>{singleProduct.title}</p>
    </div>
  );
}

export const getStaticPaths = () => {
  return {
    paths: [],
    fallback: true,
  }
}

export const getStaticProps = async ({ params }: { params: { id: string } }) => {
  const singleProduct = await singleProductApi(params.id);

  return {
    props: {
      singleProduct,
    },
    notFound: false,
  };
}

export default HomeId;

2) apis/goodsApi.ts

// apis/goodsApi.ts
export const singleProductApi = async (productId: string | undefined) => {
  try {
    const { data } = await apiRoot.get(`/${productId}`);
    return data;
  } catch (error) {
    return false; // 1번) 단, catch에서 false 넘겨줘야 한다.
    // throw error;
  }
};
  1. getServerSideProps 기본 예시
    1) pages/[id].tsx
// pages/[id].tsx
import { useRouter } from "next/router";
import { singleProductApi } from "../apis/goodsApi";
import { ProductGuard } from "../types/type";
import { NotFound } from "../components";

function HomeId({ singleProduct }: { singleProduct: ProductGuard }) {
  const router = useRouter();

  if (!singleProduct) return <NotFound />; // 1번) 없어도 에러 발생 X. 404 페이지 대신 보여주고 싶은 컴포넌트 넣으면 된다.
  return (
    <div>
      <p>HomeId 페이지</p>
      <p>HomeId: { router.query.id }</p>
      <p>{singleProduct.title}</p>
    </div>
  );
}

export const getServerSideProps = async ({ params }: { params: { id: string } }) => {
  const singleProduct = await singleProductApi(params.id);
	
  return {
    props: {
      singleProduct,
    },
    notFound: false,
  };
};

export default HomeId;

2) apis/goodsApi.ts

// apis/goodsApi.ts
export const singleProductApi = async (productId: string | undefined) => {
  try {
    const { data } = await apiRoot.get(`/${productId}`);
    return data;
  } catch (error) {
    return false; // 1번) 단, catch에서 false 넘겨줘야 한다.
    // throw error;
  }
};

  1. href 속성
    - CRA 와는 다르게, to가 아닌 href를 사용해서 연결 url을 지정해줍니다.

  2. shallow 속성
    - Next.js에서는 라우팅이 일어나면 getStaticProps, getStaticPaths, getServerSideProps이 재실행됩니다.
    - shallow 속성을 true로 설정하면 getStaticProps, getServerSideProps, getInitialProps를 실행하지 않고 url 변경을 가능하게 해줍니다.
    - 만약, 같은 페이지에서 같은 데이터를 가져오는 경우에는 해당 동작을 재실행하지 않고 새로고침을 할 수 있습니다.

  3. 기본 예시

// pages/index.tsx
import Link from "next/link"; // 1번)

function Home() {
  return (
    <div>
      <p>이곳은 메인 페이지입니다.</p>
      <Link href="/blog"> // 2번) CRA 와는 다르게, to가 아닌 href를 사용한다
        Blog
      </Link>
      <br />

      <Link href="/blog/first" shallow={true}> // 3번) 새로고침 없이 url을 바꿔준다
        Blog first
      </Link>
      <br />

      <Link href="/1">
        HomeId
      </Link>
      <br />
    </div>
  );
}

export default Home;

useRouter

// pages/[id].tsx
// import { useParams } from 'react-router-dom'; // 1번) CRA 에서는 useParams를 사용했지만
import { useRouter } from "next/router"; // 2번) Next.js 에서는 useRouter를 사용합니다.

function HomeId() {
  // CRA 버전
  // const params = useParams();
  // console.log(params); // 결과가 { id: '3' } 이런식으로. (이때, params.id는 typeof string)

  // Next.js 버전
  const router = useRouter(); // 3번)
  console.log(router.query); // 4번) 파일명을 id로 설정했기때문에, 결과가 { id: '3' } 이런식으로 나옵니다.
  // params === router.query
  // params.id === router.query.id

  return (
    <div>
      <p>HomeId 페이지</p>
      <p>HomeId: { router.query.id }</p>
    </div>
  );
}

export default HomeId;

결론: Next.js vs CRA

  1. Next.js의 장점
    - 검색 엔진 최적화에 유리합니다.
    - 추가적인 설정없이 SSR을 사용할 수 있습니다.
    - 자동으로 코드 스플리팅을 하기 때문에 초기 로딩속도가 빠르고, 특정 페이지에서 에러가 발생해도 다른 나머지 페이지들에 영향을 주지 않습니다.

  2. Next.js의 단점
    - 단점으로는 페이지를 요청할 때 마다 새로고침으로 깜빡임이 일어나 사용자 경험을 떨어뜨립니다.
    - 매번 서버에 요청하기 때문에 서버 부하가 있을 수 있습니다.
    - ESlint 설치 및 셋팅을 수동으로 해야합니다.

  3. 결론은?
    CRA와 비교한 Next.js의 장점과 단점을 통해 프로젝트 별로 선택해서 사용하는 것을 추천합니다.

profile
JS, TS, React, Vue, Node.js, Express, SQL 공부한 내용을 기록하는 장소입니다

0개의 댓글