Next.js
리액트는 기본적으로 SPA이고, CSR(클라이언트 사이드 렌더링)로 동작합니다.
클라이언트 사이드 렌더링은 번들된 자바스크립트 파일 하나를 받는데, bundle.js 파일은 리액트 코드가 자바스크립트 코드로 변환되어 들어가있는 자바스크립트 파일입니다.
브라우저가 이 자바스크립트 코드를 실행해서 DOM을 그리게 되는데, 그러다 보니 서버에서 내려받는 HTML은 비어있는 상태가 됩니다.
구글을 제외한 다른 검색 엔진에서는 이 자바스크립트 코드를 실행하지 않고 HTML만 보고 검색 엔진 알고리즘을 돌리기 때문에 검색 엔진 최적화(SEO)에 불리합니다.
이를 해결하기 위해 첫 페이지에는 SSR 즉, 완성된 HTML을 받아야겠다는 생각을 하게 되었고 효율적으로 개발하기 위해서 Next.js라는 프레임워크가 인기를 얻게 되었습니다.
설치 방법
// JS
npx create-next-app [프로젝트명]
// TS
npx create-next-app [프로젝트명] --typescript
src 디렉토리가 존재하지 않습니다.
- CRA에서 src 디렉토리에 넣었던 하위 디렉토리들을 루트에 바로 생성하면 됩니다.
루트에 생성하는 디렉토리
- 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
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 대신에 원하는 식별자 넣어도 됩니다 (동적 페이지).
참고: api 디렉토리
- pages 디렉토리의 api 폴더는 api 통신을 위한 파일들을 모아놓은 곳 입니다.
- pages 하위의 다른 폴더는 라우팅이 가능하지만, api 폴더는 라우팅이 불가능합니다 (url로 접근이 불가능 합니다).
- 삭제하고 루트에 apis 디렉토리 생성 후 사용하는 것을 추천합니다.
_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;
assets 디렉토리와 같은 역할
- public 디렉토리의 하위 폴더는 images, fonts 등을 사용하면 됩니다. (assets와 같은 방식)
기본 예시
// 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;
pre-render: 미리 HTML을 만드는 방식
- getStaticProps: 고정된 페이지를 렌더링. 수정 불가능. user의 액션 적용 X.
- getStaticPaths: 동적 라우팅이 필요할 때 일반적으로 사용.
- getServerSideProps: 변경이 잦은 페이지를 렌더링. 성능 이슈 때문에 반드시 필요할 때만 사용.
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;
// 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;
}
};
// 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;
}
};
href 속성
- CRA 와는 다르게, to가 아닌 href를 사용해서 연결 url을 지정해줍니다.
shallow 속성
- Next.js에서는 라우팅이 일어나면 getStaticProps, getStaticPaths, getServerSideProps이 재실행됩니다.
- shallow 속성을 true로 설정하면 getStaticProps, getServerSideProps, getInitialProps를 실행하지 않고 url 변경을 가능하게 해줍니다.
- 만약, 같은 페이지에서 같은 데이터를 가져오는 경우에는 해당 동작을 재실행하지 않고 새로고침을 할 수 있습니다.
기본 예시
// 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;
// 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의 장점
- 검색 엔진 최적화에 유리합니다.
- 추가적인 설정없이 SSR을 사용할 수 있습니다.
- 자동으로 코드 스플리팅을 하기 때문에 초기 로딩속도가 빠르고, 특정 페이지에서 에러가 발생해도 다른 나머지 페이지들에 영향을 주지 않습니다.
Next.js의 단점
- 단점으로는 페이지를 요청할 때 마다 새로고침으로 깜빡임이 일어나 사용자 경험을 떨어뜨립니다.
- 매번 서버에 요청하기 때문에 서버 부하가 있을 수 있습니다.
- ESlint 설치 및 셋팅을 수동으로 해야합니다.
결론은?
CRA와 비교한 Next.js의 장점과 단점을 통해 프로젝트 별로 선택해서 사용하는 것을 추천합니다.