NextJS13(2)

김현진·2023년 9월 15일
0
post-thumbnail

Nextjs를 우선 먼저 설치하고 실습을 진행해 보겠습니다. 우선 NextJS 공식문서에서 추천하는 npx create-next-app@latest를 이용해서 프로젝트를 설치하면 좋습니다.

npx create-next-app@latest로 설치하니깐 Hot-Reloading이 적용이 안되서 수동으로 설치해서 실습을 진행했습니다.

  "dependencies": {
    "@types/node": "20.6.0",
    "@types/react": "18.2.21",
    "@types/react-dom": "18.2.7",
    "autoprefixer": "10.4.15",
    "next": "^13.4.7",
    "postcss": "8.4.29",
    "prettier": "^3.0.3",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "tailwindcss": "3.3.3",
    "typescript": "5.2.2"
  }

터미널에 설치를 하면 아래 이미지처럼 뜨는데 기호에 맞게 설정하면 됩니다.

1. 프로젝트 구조

프로젝트를 생성할때 App Router를 사용한다고 하면 app 폴더가 생성이 됩니다. 기본 폴더구조는 app, src, public 폴더가 생성이 됩니다.

핵심폴더는 App Router를 사용하면 app 폴더입니다. app폴더안에서 기본적으로 loading 페이지, 공통 layout 처리, error 페이지, not-found, page를 손쉽게 처리가 가능합니다.

프로젝트 실행방법은 개발환경일때는 npm run dev로 실행하면 되고 배포환경에서는 빌드 후(npm run dev) npm run start로 실행하면 빌드한 파일로 실행이 됩니다.

2. Routing

13버전 이전에는 pages라는 폴더에서 파일을 생성하면 라우팅 처리가 되었지만 13버전에서 App Router로 프로젝트를 생성하였으면 app 폴더내에서 처리가 가능합니다. (일부 페이지들은 pages 폴더에 라우팅 처리를 해도 되지만, 앱 라우터는 페이지 라우터보다 우선순위가 높습니다.) app Router를 이용해서만 라우팅 처리하는걸 알아보겠습니다.

13버전 이전에는 page별 랜더링을 했다면 13버전에서는 컴포넌트별로 랜더링을 할 수 있습니다.

라우팅 예제로 /about, /posts 페이지를 만들어보고 다이나믹 라우트로 /posts/1 예제도 만들어 보겠습니다.

App Router인 경우에는 app 폴더하위에 만들고자 하는 url path를 지정하면 됩니다. 예를 들어 /about을 처리하고 싶으면 app > about > page.tsx(jsx) 를 만들면 알아서 라우팅 처리가 됩니다. 꼭 page.tsx(js,jsx)로 만들어야 합니다. 이렇게만 하면 알아서 라우팅 처리가 됩니다.

리액트처럼 따로 라우팅처리를 하지 않아도 파일 생성만으로 라우팅 처리가 된걸 알 수 있습니다.

export default function AboutPage() {
    return (
        <h1>About Page</h1>
    )
}

또 중첩라우팅을 하고 싶다면 폴더 구조를 계층적으로 가져가면 됩니다. 예를 들어 /about/me라는 라우팅을 처리하고 싶으면 아래 이미지 처럼 app > about > me > page.tsx 로 처리를 하면 됩니다.

알고 있으면 좋은점은 Next.js에서는 빌드 할때 따로 설정을 하지 않고 정적인 페이지라면 SSG로 빌드를 합니다. 개발할때는 SSG로 랜더링이 되지 않고 SSR로 랜더링이 됩니다. 이후에 빌드할때 좀 더 자세히 설명하도록 하겠습니다.

다이나믹 라우팅에 대해서 알아보도록 하겠습니다. 게시글을 보여주는 페이지일때 보통 /posts/1, /posts/2 이런식으로 표현을 많이 하는데 보통은 저 페이지들을 전부 만들어 놓을 수는 없으니깐 이럴때 다이나믹 라우팅을 사용합니다. 위에 라우팅처리랑 비슷하게 폴더(파일)을 생성하면 됩니다. posts/1 이렇게 만들고 싶다면 app > posts > [slug] > page.tsx 이렇게 만들면 됩니다. [slug] 여기에서 slug 대신 사용하고 싶은 id, name 이런식으로 처리를 해도 됩니다.

type Props = {
    params: {
        slug: string;
    }
}

export default function PostPage({ params: { slug }}: Props) {
    return (
        <h1>{slug} Post</h1>
    )
}

[[slug]] 이렇게 폴더를 만들었기때문에 props로 넘어오는 params 객체안에 slug가 넘어옵니다. [[id]]로 만들었으면 id가 넘어오겠죠? 저값을 가지고 보통 서버로 요청을 해서 게시글을 가져오기를 많이 합니다.

앞서 언급했듯이 정적인 페이지들은 빌드할때 SSG로 랜더링이 된다고 했습니다. 하지만 이런 다이나믹 라우팅을 처리한 페이지들은 하나하나 빌드할때 페이지를 만들어 놓을 수 없으니 SSR로 사용자가 요청이 오면 페이지를 만든다는것을 알고 있으면 좋습니다. 하지만 특정 게시글을 빌드할때 미리 만들어 놓고 싶으면 generateStaticParams 함수를 이용해서 빌드할때 미리 만들어 놓으수 있습니다.

type Props = {
    params: {
        slug: string;
    }
}

export default function PostPage({params: { slug }}: Props) {
    return (
        <h1>{slug} Post</h1>
    )
}

export async function generateStaticParams() {
    const posts = ['1', '2']
    return posts.map((post) => ({
        slug: post,
    }))
}

지금은 하드코딩으로 처리를 했지만 보통은 서버에서 받아온 데이터를 처리하는 방식으로 하면 됩니다. slug 키를 가지는 배열을 리턴하면 됩니다. 주의사항으로는 값을 문자열로 처리해야 빌드할 때 에러가 발생하지 않습니다.

실제 빌드를 하면 나오는 화면인데 원으로 나오면 SSG로 처리했다는것을 알 수 있습니다. 아래처럼 람다표시가 있으면 SSR로 런타임에 페이지를 빌드해서 클라이언트로 내려준다는것을 알 수 있습니다.

이외에도 좀더 복잡한 url을 처리하고 싶을때 [...slug], [[..slug]]를 이용해서 처리하면 됩니다. 둘의 차이점을 보면 [[]] 대괄호가 두개가 있는건 /posts 요기에도 매칭이 됩니다. /posts, /posts/1/2 , /posts/1 모든구간에 매칭이 가능

공식문서 Routing

3. Layout

프로젝트를 하다 보면 공통적으로 거의 모든페이지에서 필요로 하는 Header, Footer 같은경우 컴포넌트로 만든 다음 layout 설정을 합니다. Next.js 13버전에는 파일컨벤션으로 layout 설정이 가능합니다. 프로젝트를 생성하면 Root Layout이 생성이 되는데 이건 모든 페이지에 적용이 되는 layout입니다. Root Layout일 경우에는 html, body가 포함이 되어야 하고 Root Layout은 무조건 필요로 합니다. Root Layout의 경우는 서버컴포넌트로 구성해야 하며 클라이언트 컴포넌트로 구성할 수 없습니다. 기존에 있는 _app.js, _document.js 파일이 root layout으로 대체되었다고 생각하시면 됩니다.

그런데 만약 특정페이지에서는 공통 레이아웃인 헤더를 제거를 하고 싶을때 가 있습니다. 이럴경우엔 방법이 여러가지가 있는데 한가지방법으로는 레이아웃을 컴포넌트화 시켜서 컴포넌트안에서 next에서 제공하는 usePathname hook을 이용해서 처리하면 됩니다. hook을 사용할려면 클라이언트 컴포넌트로 만들어야 하는데 Root Layout 전체를 클라이언트 컴포넌트로 만들어버리면 에러가 발생하고 만약 된다고 하더라도 전체를 클라이언트 컴포넌트로 만들어버리면 비효율적입니다.

'use client';
import React from 'react';
import { usePathname } from 'next/navigation';
type Props = {
  children: React.ReactNode;
};
export default function BaseLayout({ children }: Props) {
  const pathname = usePathname();

  return (
    <>
      {pathname !== '/about' && <h1>Header</h1>}
      {children}
    </>
  );
}

// layout.tsx

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <BaseLayout>{children}</BaseLayout>
      </body>
    </html>
  );
}

이런식으로 BaseLayout이라는 컴포넌트를 만들어서 처리를 할 수 있습니다. 클라이언트 컴포넌트로 만들려면 최상단에 use client 문구를 적어줘야 hook이나 event 처리가 가능합니다.

이외에도 처리하는 방법 Routing Group를 이용해서 할 수 있습니다.

그리고 특정페이지에서 공통레이아웃을 작성 할 경우도 있습니다. 이럴경우엔 해당 폴더내에서 layout 파일을 작성하면 됩니다. 루트레이아웃 안에 또 생성이 된다고 생각하시면됩니다.

// 간략하게 컴포넌트로 그려보면 이런식으로 랜더링이 된다고 생각하시면 됩니다.

<RootLayout>
  <PostLayout>
    <PostPage />
  </PostLayout>
</RootLayout>

// > app > posts > layout.tsx
// 이렇게 작성할경우 posts 안에 폴더 내부에 있는 파일들은 layout이 적용이 됩니다.
export default function PostLayout({ children }: { children: React.ReactNode }) {
  return (
    <>
      <div>
        <span>tab1</span>
        <span>tab2</span>
      </div>

      {children}
    </>
  );
}

4. Linking and Navigating

nextjs에서 페이지간 이동을 할때는 다른 라이브러리 설치가 필요없이 를 next/linkd에서 불러와서 사용하면 됩니다. Link 태그의 특징은 default props로 prefetch 값이 true로 되어 있어서 해당 페이지에 Link로 되어 있는 페이지가 있으면 페이지를 미리 데이터를 가져옵니다. href 속성으로 이동할 페이지로 이동시키면 되고 또 만약 다른 외부 페이지로 이동시키고 새탭으로 열고 싶으면 a 태그를 사용할 필요없이 Link 태그의 target="_blank"를 넣어서 해도 됩니다.

prefrech가 되는지 확인을 할려면 빌드를 한 후 에 실제 배포환경에서 확인을 해야 확인이 가능합니다.

또는 page가 이동되면 scroll이 최상단으로 올라가는걸 방지 할려면 scroll props를 false로 하면 됩니다. default 값은 true입니다.

 <Link href="/about" >About</Link>
 <Link href="https://www.naver.com" target="_blank">Naver</Link> 

만약 Link 태그를 사용 못 할경우엔 nextjs에서 제공해주는 useRouter hook을 사용하면 됩니다.(next/navigation에서 불러와서 사용하면 됩니다.)

단 주의점은, hook을 사용할 경우에는 클라이언트 컴포넌트로 만들어야 합니다. 클라이언트로 컴포넌트를 사용을 할려면 'use client' 문구를 최상단에 적어주면 되고, nextjs13 에서는 기본적으로 서버컴포넌트로 랜더링이되고 서버컴포넌트에 대해서는 다음 블로그 포스틩에 다뤄보도록 하겠습니다. Link 컴포넌트를 사용을 할 수 있으면 Link 컴포넌트로 페이지 이동을 시키면 됩니다.

'use client';

import {useRouter} from "next/navigation";

const router = useRouter();
  
    return (
        <>
            <nav>
                <span onClick={() => router.push('/profile')}>profile</span>
            </nav>
        </>
    );    

5. Loading UI and Streaming

next.js에서 Loading UI를 손쉽게 구현이 가능하도록 제공해줍니다. 위에 layout 처럼 파일이름을 loading.js[tsx]로 작성을 하면 됩니다. loading 스피너나 스켈레톤 UI를 보여주면 되는데 사용할 페이지 폴더에 loading.js[tsx] 파일을 만들면 됩니다. loading.js를 사용하면 내부적으로 React Suspense 를 사용해서 loading.js를 구현이 되어 있습니다.

Suspense 안에 있는 하위 컴포넌트(children)가 로드가 완료될때 까지 기다렸다가 대체항목(fallback UI)를 보여줄 수 있습니다.

보통 비동기 통신에서 데이터를 가져올때 사용하거나 코드스플리팅을 사용할때 Suspense를 사용합니다. 그리고 loading.js는 layout.js 내부에 중첩되게 위치 되어지고 Suspense가 Page를 감싸는 형태를 나타냅니다.

내부적으로는 저런식으로 구현이 되고 좀 주의 깊게 볼껀 Layout은 유지가 되고 Page쪽만 Suspense의 영향을 받는걸 알수 있습니다. 즉 다시말해 Layout은 보여지고 Page 컴포넌트가 랜더링이 될때까지 기다렸다가 로드가 되면 그때서야 Page 컴포넌트가 보여지는거고 그전에는 fallback UI가 보여집니다. Loading 컴포넌트가 loading.js라고 생각하시면 됩니다. loading.js도 각 페이지마다 사용이 가능이 가능합니다.

만약 Page 컴포넌트가 로드가 되는데 오래 걸린다면 로딩 UI를 사용자가 오랫동안 보게 될것입니다. 그렇게 하지않고 중용한 데이터는 먼저보여주고 덜 중요한 데이터는 나중에 보여주게 병렬적으로 보여주면 사용자 입장에서는 좀 더 서비스를 이용하는데 편할겁니다. 하지만 loading.js는 Page컴포넌트를 다 감싸는 형태이므로 Page 컴포넌트의 덩치(?)가 크면 클수록 사용자는 늦게 페이지를 보게 될겁니다. 그럴때 아까 말한 병렬적으로 컴포넌트를 랜더링 하면 됩니다.

위에 문제를 해결하기 위해 Streaming을 사용하면 됩니다. 스트리밍을 사용한다기 보다는 Suspense를 좀 더 세부적으로 나눈다고 하는게 좋은거 같습니다.

SSR 랜더링은 위 그림처럼 동기적으로 랜더링이 되는 방식입니다. 스트리밍을 사용하면 페이지의 HTML을 더 작은 단위로 나누고 점진적으로 해당 데이터를 서버에서 클라이언트로 보냅니다. 이를 통해 UI가 랜더링도기 전에 모든 데이터가 로드 될 때까지 기다리지 않고 페이지의 일부를 더 빨리 표시할 수 있습니다.

스트리밍을 사용하면 TTFB, FCP 줄일 수 있고, TTI도 향상이 됩니다.

import { Suspense } from 'react';

export const fetchData = (delay) => new Promise((resolve) => setTimeout(() => resolve('data'), delay));

const AComponent = async () => {
  await fetchData(3000);

  return <h1>AComponent</h1>;
};

const BComponent = () => {
  return <h1>B Component가 중요하니깐 먼저 보여주자</h1>;
};

export default async function PostPage() {
  return (
    <>
      <div>PostPage</div>
      <BComponent />

      <Suspense fallback={<h1>wait 3seconds....</h1>}>
        <AComponent />
      </Suspense>
    </>
  );
}  

Suspense로 AComponent를 처리했을때는 먼저 BComponent 중요한 컴포넌트를 먼저 보여준후 AComponent가 보여지고 있습니다.

loading.tsx를 사용했을때는 3초동안 Post페이지를 확인을 하지 못하는것을 확인할 수 있습니다.

// post > loading.tsx

export default function PostLoading() {
  return <h1>Loading...</h1>;
}

6. Error Handling

Next.js 13버전에서는 error.js 파일로 인해서 error page를 다룰수 있습니다. 기존 layout, loading처럼 중첩라우팅에서 처리가 가능합니다. error.js는 내부적으로 React Error Boundary 를 사용하고 있고, 나머지 app 기능들은 유지하고(서비스 전체적으로 다운이 되지 않고) 해당페이지에서 에러처리가 가능합니다. 기존 layout, loading 글이랑 비슷해서 짧고 간략하게 하고 넘어 가겠습니다.

error.js를 만들때는 클라이언트 컴포넌트로 작성해야됩니다 그리고 error.js도 다른 layout, loading가 마찬가지로 가장 가까운 부모 error.js를 찾아서 랜더링이 됩니다.

layout.js, template.js에서 발생한 에러는 error.js에서 catch가 안됩니다. (앱 전체를 감싸는 global-error.js를 사용해서 전체적인 앱의 에러를 캐치할 수 있습니다.)

'use client';

import { useEffect } from 'react';

export default function PostError({ error, reset }: { error: Error; reset: () => void }) {
  useEffect(() => {
    // Log the error to an error reporting service
    console.error(error);
  }, [error]);

  return (
    <div>
      <h2>Something went wrong!</h2>
      <button
        onClick={
          // Attempt to recover by trying to re-render the segment
          () => reset()
        }
      >
        Try again
      </button>
    </div>
  );
}
// app/global-error.tsx
// html, body 태그가 포함되어 있어야 합니다.

'use client'
 
export default function GlobalError({
  error,
  reset,
}: {
  error: Error
  reset: () => void
}) {
  return (
    <html>
      <body>
        <h2>Something went wrong!</h2>
        <button onClick={() => reset()}>Try again</button>
      </body>
    </html>
  )
}

NextJS의 주요기능들을 알아보았고 다음 글은 랜더링 방식, 이미지 최적화, 폰트최적화에 대해서 알아보도록 하겠습니다.

profile
기록의 중요성

0개의 댓글