본문 바로가기

[Dev] 🎯Self Study

[리액트에서 Next로] Next.js 기초 : 레이아웃, 프로그래믹 네비게이션, 로딩(loading), 에러 페이지 (error 컴포넌트) 예시

 

Next.js에서 레이아웃을 이용해 공통 UI를 구축하는 방법 

 

[핵심 내용]

 

1. 레이아웃은 여러 페이지에서 공유되는 UI를 구축할 때 사용

2. 앱 폴더 내에 있는 루트 레이아웃은 모든 페이지에 공통으로 적용되는 UI를 정의,

HTML과 바디 요소를 반환하고 바디 내에서 페이지의 Children을 동적으로 렌더링 

3. 중첩된 레이아웃을 만들 수 있으며, 특정 레이아웃이 필요한 페이지를 위해 특정 레이아웃을 생성 가능

4. 레이아웃 컴포넌트가 역할을 하려면 반드시 children 프롭스를 가져야 하며,

이를 구현하기 위해 프롭스 형태를 정의하는 인터페이스를 생성

5. 관리자 레이아웃 컴포넌트를 통해 모든 관리자 페이지에 공통으로 사용되는 UI 요소를 설정 

6. 네비게이션 바는 모든 페이지에 공통으로 적용되는 레이아웃이므로 루트 레이아웃에 정의 



[실습 내용]


1. 새로운 폴더를 만들고 layout.tsx 파일을 추가


2. 프롭스를 추가하고 children 프로퍼티를 렌더링

import React from 'react'

interface Props {
    children : React.ReactNode; //칠드런 프로퍼티 값이 React 컴포넌트
}

const AdminLayout = ({children} : Props) => {
  return (
    <div>
      <div>{children}</div>
    </div>
  )
}

export default AdminLayout

 

3. 공통 UI 요소 설정과 사이드바 추가

bg-slate-200: 배경
p-5: 패딩
mr-5: 오른쪽 마진
const AdminLayout = ({children} : Props) => {
  return (
    <div className='flex'>
        <aside className='bg-slate-200 p-5 mr-5'></aside>
      <div>{children}</div>
    </div>
  )
}



 

4. 네비게이션 바 추가와 유지보수를 위해 별도의 컴포넌트로 구현

import Link from 'next/link'
import React from 'react'

const Navbar = () => {
  return (
    <div>
      <Link className='mr-5' href="/">
        Next.js
      </Link>
      <Link href="/users">Users</Link>
    </div>
  )
}

export default Navbar

 

 return (
    <html lang="en" data-theme="winter">
      <body className={inter.className}>
      <Navbar/>
      <main className='p-5'>{children}</main>
      </body>
    </html>

 

 

5. 테일윈드를 통해 각 태그의 기본으로 적용되는 "글로벌 스타일"을 변경

: 스타일을 적용할 때는 @apply 메소드를 사용 

@layer base {
  /* 기본 HTML 태그 스타일 변경 가능 @apply 메소드 사용 */
  h1 {
    @apply font-extrabold text-2xl mb-3;
  }
}

 

 

링크 컴포넌트 핵심내용

1. 타겟 페이지의 내용만 다운로드한다.

2. 뷰포트에 있는 링크를 미리 가져온다.

(단, Production Mode에서 동작)
3. 클라이언트에서 페이지를 캐싱한다.
(단, 새로고침시 다시 요청을 보냄.)

 


 

프로그래믹 네비게이션

 


Navigation


Link Component: 서버 사이드 렌더링 컴포넌트(SSR)
Programmatic Navigation: 클라이언트 사이드 렌더링 컴포넌트(CSR)

(버튼, 양식 제출 등 BrowserAPI를 사용하는 경우)

      <Link href="/users/new" className='btn btn-primary'>New User</Link>

 

 



useRouter Hook (Programmatic Navigation)
Pages Router
1. import { useRouter } from "next/router";
2. const router = useRouter();
App Router - 현재 App Router를 사용하기 때문에 이 방식을 사용

(next/router 모듈에서 제공하는 useRouter 메소드를 사용하면 오류가 출력 )

 

1. import { useRouter } from "next/navigation";
2. const router = useRouter();

 

"use client" // 클라이언트 컴포넌트로 사용 - 버튼 상호작용
import React from 'react'
import { useRouter } from 'next/navigation' // 훅 가져오기

const NewUsers = () => {
  return (
    <button className='btn btn-primary' onClick={() => {}}> Create</button>
  )
}

export default NewUsers



클릭해서 이동

 

create 클릭시 화면 전환 

"use client" // 클라이언트 컴포넌트로 사용 - 버튼 상호작용
import React from 'react'
import { useRouter } from 'next/navigation'

const NewUsers = () => {
//userRouter 객체를 통해 인스턴스 생성, 해당 라우터의 푸시 메소드 이용하여 전환
const router = useRouter();

  return (
    <button className='btn btn-primary' onClick={() => router.push("/users")}> Create</button>
  )
}

export default NewUsers

 

 

push 메소드는 프로그래메틱 네비게이션으로 클라이언트 컴포넌트에서 페이지 전환에 사용되는 메소드

 

 



React Suspense 컴포넌트 적용
 

- fallback 프로퍼티를 적용해 로딩시 렌더링 되는 UI를 작성

: 로딩 메세지는 메세지를 작성하거나, 아이콘 혹은 애니메이션 컴포넌트로 설정

import React, { Suspense } from 'react'
import UserTable from './UserTable'
import Link from 'next/link'

interface Props {
  searchParams : {sortOrder : string}
}

const UsersPage = ({searchParams : {sortOrder}} : Props) => {
  
  return (
    <div>
      <h1>this is user.</h1>
      <Link href="/users/new" className='btn btn-primary'>New User</Link>
      <p>{new Date().toLocaleTimeString()}</p>
      <Suspense fallback={<p> Loading ... </p>}>
      <UserTable sortOrder = {sortOrder}/>
      </Suspense>
    </div>
  )
}

export default UsersPage

 

- users 페이지에서 새로고침하면 아주 잠깐 로딩 메시지 보임 

서스펜스 확인하기  (확장프로그램 설치)

 

 

React Developer Tools

Adds React debugging tools to the Chrome Developer Tools. Created from revision 993c4d003 on 12/5/2023.

chrome.google.com

 

설치 안하는 걸 추천. 이유는 모르겠지만 한 번 설치하니 개발자 도구가 계속 꺼진다.... (확장 삭제 후, 껐다 켜서 해결함)

 

 

 



검색 엔진봇이 보는 페이지는 로딩 메시지가 표시된 문서로, 
서버는 이 페이지를 생성하고 클라이언트로 보냈지만 연결을 종료하지 않음


대신에 사용자 테이블 컴포넌트가 렌더링 될 때까지 대기하고 추가 데이터를 클라이언트로 전달

이를 스트리밍이라하며, 비디오나 오디오를 송출할 때와 동일한 기술 


스트리밍

서버는 사용자 페이지가 렌더링 될 때까지 대기, 추가 데이터를 클라이언트로 전달

 

Questions
1. 특정 페이지에 여러 개의 서스펜스 컴포넌트를 가진다면 어떻게  ?
2. 모든 페이지에 공통의 서스펜스 컴포넌트를 가지고 싶은 경우 어떻게 ?



해결방법

 #1: 공통 레이아웃에 자식 요소를 렌더링하는 태그를 Suspense 컴포넌트로 감싸기

(위의 예시)

 

#2. loading.tsx 파일 생성
Global Loading: app/loading.tsx


Local Loading:

ex) Users: app/users/loading.tsx
ex) Admin: app/admin/loading.tsx
ex) Products: app/products/loading.tsx

 

 

loading 파일명은 Next.js 에서 약속한 이름이기 때문에 반드시 지켜야 한다.
이는 Next.js에서 제공하는 loading.tsx 파일을 사용 

 

 

 

로딩 애니메이션 적용 

 

 

Tailwind Loading Component — Tailwind CSS Components ( version 4 update is here )

Tailwind Loading examples: Loading shows an animation to indicate that something is loading. component

daisyui.com

 

import React from 'react'

const loading = () => {
  return (
    <div>
        <span className="loading loading-ring loading-md"></span>
    </div>
  )
}

export default loading

 

 


에러 페이지 만들기


not-found.tsx
이 또한 next.js에서 명시한 규칙의 파일명을 사용

 



not-found.tsx 사용하기


Global not-found: app/not-found.tsx


Local not-found
ex) app/users/[id]/not-found.tsx 

ex) app/products/not-found.tsx 

ex) app/admin/not-found.tsx


next.js에서 명시한 규칙의 파일명을 사용 

 

 




Dynamic Routes
다음 url을 처리하고 싶은 경우 Dynamic Routes를 사용 


app/users/[id]/page.tsx

 

ex) localhost:3000/users/1
ex) localhost:3000/users/2
ex) localhost:3000/users/3 ex) localhost:3000/users/...

 

1. [id] 하위의 page.tsx 에서 if문 작성

 

    if (id > 10) //10보다 크면 notFound 호출

 

2. not-found.tsx 생성

 

import React from 'react'

const UserNotFound = () => {
  return (
    <div>
      <h1>10보다 큰 사용자를 찾을 수 없습니다.</h1>    
      </div>
  )
}

export default UserNotFound

 

3.  next/navigation 적용하기

next/navigation를 사용하면

not-found, loading 등의 next.js에서 지정한 파일을 마치 함수처럼 호출  
ex) import { notFound } from "next/navigation"

import { notFound } from 'next/navigation';
import React from 'react'
...
const UserDetailpage = ({params : {id}}: Props) => {
    // console.log(props.params.id);

    if (id > 10) notFound();//10보다 크면 notFound 호출 
...

export default UserDetailpage

 

10 이하에서 정상작동
10이상 입력시 에러 호출

 



컴포넌트에서 에러 처리


error.tsx
not-found.tsx 파일명과 같이 next.js에서 지정한 error.tsx라는 파일명을 사용 

 



이전의 Not Found 컴포넌트와 같이 오류 메시지를 원하는 방식으로 작성.

 

예상치 못한 오류 메시지를 직접 작성하고 싶은 경우 error.tsx 파일을 사용

내부적으로 예상치 못한 오류 가 발생했을 때 파일 내부에 정의된 컴포넌트가 렌더링 


하지만 error.tsx 컴포넌트는 반드시 클라이언트 컴포넌트로 만들어야 하는 점을 주의

-  error.tsx 파일을 생성하고 최상단에 use client 지시문 작성


이유는?

error.tsx 파일에서는 새로 고침을 하는 등 브라우저 API와 상호작용해야 하기 때문.

 

 


1. error.tsx 파일 생성 

 

2. use client 지시문 정의 

3. 오류메시지 정의 (예상치 못한 오류가 발생했습니다.)

"use client";

import React from 'react'

const ErrorPage = () => {
  return (
    <div>
        예상치 못한 오류가 발생했습니다.
    </div>
  )
}

export default ErrorPage

 

usertable.tsx 파일에서 주소에 오류값 넣으면

    const res = await fetch("https://jsonplaceholder.typicode.com/userszzz", {cache: "no-store"})

 



Global Unexpected Error : app/error.tsx


이때, app/layout.tsx에서 발생한 오류는 캐치하지 못하는 점을 명심 
만약 app/layout.tsx에서 발생한 오류를 캐치하고 싶은 경우, global-error.tsx 파일을 사용

이 또한 next.js에서 명시한 파일명이기 때문에 반드시 이름을 명시


Local Unexpected Error:
ex) app/users/error.tsx
ex) app/products/error.tsx
ex) app/admin/error.tsx

 

오류메세지는 error.tsx 컴포넌트의 인자 값 props로 전달 

const ErrorPage = (props) => console.log.(props.error)

 

error 관련 사이트 

( Sentry : 어플리케이션에서 오류가 발생하면 알려주는 에러 트래킹 서비스 )

 

 

Application Performance Monitoring & Error Tracking Software

Self-hosted and cloud-based application performance monitoring & error tracking that helps software teams see clearer, solve quicker, & learn continuously.

sentry.io

 

 

리로드 하고 싶다면?


reset 메소드를 통해 사용자가 오류가 출력된 페이지를 재로드 

"use client";

import React from 'react'

interface Props {
    error : Error;
    reset : () => void; //return 값이 없으므로 void 속성
}

const ErrorPage = ({error, reset} : Props) => {
    console.log("Error: ", error);
  return (
    <>
        <div>
        예상치 못한 오류가 발생했습니다.
        </div>
        <button className='btn' onClick={() => reset()}>Reset</button>
    </>

  )
}

export default ErrorPage

 

 

주의


reset 메소드를 과도하게 사용하면 불필요한 오류 로그가 여러 번 기록

이는 사용자 경험에도 부정적인 영향을 미칠 수 있으므로, 꼭 필요한 경우에만 사용하는 것이 바람직