728x90
반응형

getServerSideProps

만약 getServerSideProps 라 불리는 async 함수를 export한다면 Next.js는 getServerSideProps에 의해 리턴된 data를 사용하면서 이 페이지를 매 요청시마다 pre-rendering 할 것이다.

 

export async function getServerSideProps(context) {
  return {
    props: {}, // will be passed to the page component as props
  }
}

 

context 파라미터는 다음 속성들을 포함하는 객체이다.

params 만약 동적 라우트를 사용하는 페이지라면 params는 라우트 파라미터들을 포함한다. 만약 페이지명이 [id].js라면 params{id : ...}와 같은 형태가 될 것이다.
req HTTP IncommingMessage object
res HTTP response object
query An object representing the query string
preview previewtrue라면 이 페이지는 preview모드, false면 아닌 것이다.
previewData setPreviewData에 의해 세팅된 preview data이다
resolvedUrl client transition을 위해 _next/data 접두어가 생략된 요청 url의 정규화 버전
locale active locale을 포함
locales 모든 지원 locales를 포함
defaultLocale 디폴트로 설정된 locale

 

getServerSideProps가 리턴하는 객체는 아래항목들이 포함한다.

  • props - 페이지 컴포넌트에 의해 리턴되는 props는 필수객체이다.
  • notFound - 페이지가 404 상태와 404 페이지로 리턴되도록 허용하는 옵셔널한 boolean 값이다.
  • redirect: 옵셔널한 값으로 내외부 자원으로 redirecting 해주는 값이다. 반드시 {destination: string, permanent: boolean} 형태여야 한다.

 

export async function getServerSideProps(context) {
  const res = await fetch(`https://.../data`)
  const data = await res.json()

  if (!data) {
    return {
      redirect: {
        destination: '/',
        permanent: false,
      },
    }
  }

  return {
    props: {}, // will be passed to the page component as props
  }
}

 

 

언제 getServerSideProps를 써야할까?

getServerSideProps는 pre-render되는 페이지의 필요한 데이터가 요청시에 가져와야하는 경우에만 사용해라. 서버가 반드시 모든 요청에 대한 결과를 계산해야만 하기 때문에  getStaticProps보단 느리다.

 

만약 데이터가 pre-render 될 필요가 없다면, client-side에서 데이터를 가져오는 것을 고려해봐라.

 

  • 동적 라우트에서 정적인 데이터를 필요로 할 때 : getStaticProps
  • 동적 라우트에서 정적인 데이터를 필요로 하며, 정적인 페이지를 생성할 때 : getStaticPaths with getStaticProps
  • pre-render되는 페이지에 필요한 데이터를 요청시에 가져와야 하는경우 : getServerSideProps
  • pre-render가 필요없고 데이터를 가져와야하는 경우 : Axios

 

TypeScript : Use GerServerSideProps

타입스크립트를 사용할 때에는 next에서 GetServerSideProps를 사용할 수 있다.

 

import { GetServerSideProps } from 'next'

export const getServerSideProps: GetServerSideProps = async (context) => {
  // ...
}

 

 

Technical details

Only runs on server-side

getServerSideProps는 서버에서만 동작하고 브라우저에서 동작하지 않는다. 사용할 때

  • getServerSideProps는 요청시에만 실행된다.
  • 만약 이 페이지가 next/Linknext/router를 통해 요청된다면 Next.js는 API요청을 서버에 보낸다. 그리고 getServerSideProps를 실행한다. 그리고 getServerSideProps의 실행결과로 만들어진 JSON을 리턴한다. 

 

또한 getServerSIdePropspages에서만 요청된다.

 

 

SWR

리액트에서 쓰이는 useAsync같은 Hook이다. 클라이언트사이드에서 데이터를 가져올 때 이것이 매우 유용하다.

 

import useSWR from 'swr'

const fetcher = (url) => fetch(url).then((res) => res.json())

function Profile() {
  const { data, error } = useSWR('/api/user', fetcher)

  if (error) return <div>failed to load</div>
  if (!data) return <div>loading...</div>
  return <div>hello {data.name}!</div>
}

 

 

728x90
반응형
728x90
반응형

getStaticPaths(Static Generation)

만약 dynamic routes를 가지고 있는 페이지가 있고, 그 페이지가 getStaticProps를 사용한다면 그 페이지는 빌드시에 HTML pre-render될때 어떤 경로의 리스트의 페이지들인지 정의할 필요가 있다.

 

dynamic routes를 사용하는 어떤 페이지에서  getStaticProps라 불리는 async 함수를 export한다면, Next.js는 

getStaticProps에 의해 정의된 모든 paths들을 정적으로 pre-render 할 것이다.

 

export async function getStaticPaths() {
  return {
    paths: [
      { params: { ... } } // See the "paths" section below
    ],
    fallback: true or false // See the "fallback" section below
  };
}

 

 

The paths key(required)

path키는 pre-render되야하는 경로들을 정의한 것이다. pages/posts/[id].js 라는 이름의 dynamic routes를 사용하는 페이지가 있다고 가정해보자.  getStaticPaths를 해당 페이지에서 export하고 paths를 다음과 같이 리턴한다.

 

return {
  paths: [
    { params: { id: '1' } },
    { params: { id: '2' } }
  ],
  fallback: ...
}

 

그럼 Next.js는 posts/1, posts/2 를 빌드시에 정적으로 생성하고, pages/posts/[id].js의 페이지 컴포넌트로서 사용된다.

 

  • 만약 페이지 명이 pages/posts/[postId]/[commentId] 라면 params는 반드시 postId와 commentId를 포함해야한다.
  • 만약 pages/[...slug] 라고 한다면 params는 반드시 slug를 배열로 포함해야한다.

 

The fallbackkey (required)

getStaticPaths에 의해 리턴되는 객체는 반드시 boolean속성의 fallback 키를 포함해야한다.

 

fallback: false

 

만약 fallbackfalse이면 getStaticPaths에 의해 전달받지 못한 path들은 모두 404 페이지로 호출된다.

새 페이지가 자주 추가되지 않는다면 이방식은 유용하다. 만약 데이터 소스에 항목을 더 추가하고 새 페이지를 렌더링해야하는 경우엔 빌드를 다시 실행해야 한다.

 

아래 페이지를 예로 들어보자. getStaticPath에 의해 CMS로 부터 블로그 포스트리스트는 반환된다. 그리고 각 페이지들은 getStaticProps 에 의해 CMS로부터 각 포스트 데이터가 반환된다.

// pages/posts/[id].js

function Post({ post }) {
  // Render post...
}

// This function gets called at build time
export async function getStaticPaths() {
  // Call an external API endpoint to get posts
  const res = await fetch('https://.../posts')
  const posts = await res.json()

  // Get the paths we want to pre-render based on posts
  const paths = posts.map((post) => ({
    params: { id: post.id },
  }))

  // We'll pre-render only these paths at build time.
  // { fallback: false } means other routes should 404.
  return { paths, fallback: false }
}

// This also gets called at build time
export async function getStaticProps({ params }) {
  // params contains the post `id`.
  // If the route is like /posts/1, then params.id is 1
  const res = await fetch(`https://.../posts/${params.id}`)
  const post = await res.json()

  // Pass post data to the page via props
  return { props: { post } }
}

export default Post

 

만약 fallback이 true이면 getStaticProps의 행동이 변한다.

 

  • getStaticPaths로 부터 리턴된 경로들은 getStaticProps에 의해 빌드시에 HTML로 render된다.
  • 빌드시에 생성되지 않은 경로들은 404페이지로 로드되지 않는다. 대신 첫번째요청시에 "fallback"버전을 제공한다.
fallback: true는 next next 사용시 지원되지 않는다.

 

Fallback Pages: 로딩페이지 같은 역할

"fallback" 버전의 페이지는

  • 페이지의 props는 비워진다.
  • 라우터사용시 fallback이 렌더되면 router.isFallbacktrue값이 되는데 이로인해 fallback이 렌더링 되었다는 것을 확인할 수 있다.
// pages/posts/[id].js
import { useRouter } from 'next/router'

function Post({ post }) {
  const router = useRouter()

  // If the page is not yet generated, this will be displayed
  // initially until getStaticProps() finishes running
  if (router.isFallback) {
    return <div>Loading...</div>
  }

  // Render post...
}

// This function gets called at build time
export async function getStaticPaths() {
  return {
    // Only `/posts/1` and `/posts/2` are generated at build time
    paths: [{ params: { id: '1' } }, { params: { id: '2' } }],
    // Enable statically generating additional pages
    // For example: `/posts/3`
    fallback: true,
  }
}

// This also gets called at build time
export async function getStaticProps({ params }) {
  // params contains the post `id`.
  // If the route is like /posts/1, then params.id is 1
  const res = await fetch(`https://.../posts/${params.id}`)
  const post = await res.json()

  // Pass post data to the page via props
  return {
    props: { post },
    // Re-generate the post at most once per second
    // if a request comes in
    revalidate: 1,
  }
}

export default Post

 

 

When is fallback : true Useful?

fallback: true 는 app이 매우 많은 양의 정적페이지를 가지고 있을때 유용하다. 어떤 사람들이 아직 생성되지 않은 페이지들을 요청할 수 있다. 그때 그 유저는 로딩창을 볼 수 있을것이다. 다만 생성된 페이지들을 업데이트 하지 않는다.

업데이트하려면 ISR을 fallback true와 같이 쓰도록 하자.

 

fallback : 'blocking'

 

만약 fallback이 blocking이라면 getStaticPath에 의해 리턴되지 않은 새로운 페이지들은 HTML이 생성될 때 까지

기다릴 것이다. 원래는 데이터 받고 HTML이 생성되지만 그 반대로 작동한다.

  • getStaticPaths로 부터 반환된 경로들은 getStaticProps에 의해 빌드시 HTML로 render된다.
  • 빌드시에 생성되지 않은 경로들이 404페이지로 가지 않는다. 
  • loading/fallback 상태가 없고 그냥 요청된 페이지가 바로 로드된다.
fallback : 'blocking'은 next export 사용시 지원되지 않는다.

 

When should I use getStaticPaths?

동적 라우트를 사용해서 페이지를 정적으로  pre-rendering한다면 ( pages/[id].js ) 반드시 getStaticPaths를 써야한다.

 

 

TypeScript: Use GetStaticPaths

타입스크립트에서는 next에서 GetStaticPaths를 import 할 수 있다.

 

import { GetStaticPaths } from 'next'

export const getStaticPaths: GetStaticPaths = async () => {
  // ...
}

 

 

Technical details

  • getStaticProps를 동적 라우트 파라미터와 함께 사용한다면 getStaticPaths는 무조건 써야한다. getStaticPathsgetServerSideProps와 절대 같이 사용할 수 없다.
  • getStaticPaths는 서버사이드에서 빌드시에만 실행된다.
  • getStaticPathspage에서만 exported 될 수 있다. page가 아닌곳에선 export 할 수 없다.
  • 개발환경에서는 getStaticPaths는 매 요청시에 호출된다.
728x90
반응형
728x90
반응형

ISR(Incremental Static Regeneration)

ISR(Incremental Static Regeneration) 을 사용하면 트래픽이 들어올 때 백그라운드에서 다시 렌더링해서

기존 페이지를 업데이트 할 수 있게 해준다. 아래와같이 코드를 작성해보자.

 

about.js

function About({posts}) {
  console.log(posts)
    return (
      <ul>
        {posts.map((post) => (
          <li key={post.id}>{post.name}</li>
        ))}
      </ul>
    )
  }
  
export default About

export async function getStaticProps(context) {
    const res = await fetch(`https://jsonplaceholder.typicode.com/users`)
    const posts = await res.json()

  return {
    props: {
      posts,
    },
    // Next.js will attempt to re-generate the page:
    // - When a request comes in
    // - At most once every second
    revalidate: 1, // In seconds
  }
}

 

실행해보자.

 

 

 

이제 블로그의 포스트리스트는 1초마다 재생성된다. 만약 새로운 블로그 포스트가 추가되면 앱을 재빌드하거나 재배포없이 거의 곧바로 활용가능해진다.

 

이것은 또한 fallback:true 옵션과 완벽히 동작한다.

 

Static content at scale

전통적인 SSR과는 다르게 ISR은 정적특징의 장점을 보장해준다.

  • 지연시간이 급증하지않고, 페이지는 지속적으로 빠르게 제공된다.
  • 페이지가 오프라인이 되지 않으며 만약 재생성이 실패해도 예전 페이지는 변경되지 않고 유지된다.
  • db나 백엔드에 로드 부하가 작다. 페이지들이 동시에 한번 재계산되기 때문이다.

 

파일 읽기 : process.cwd()

파일들을 getStaticProps 에서 직접 파일시스템을 통해 읽혀 질 수 있다. 그러기 위해선 반드시 파일의 full path를 가지고 있어야 한다.

 

Next.js는 코드를 몇개의 디렉토리 속으로 나누어 컴파일한다. 따라서 __dirname을 사용하면 실제 페이지 디렉토리 경로와 다른 경로를 리턴하기 떄문에 사용할 수 없다.

 

대신 우리는 process.cwd()를 사용할 수 있다. 이것은 Next.js 실행이 일어나는 디렉토리 정보를 준다.

 

about.js

import { promises as fs } from 'fs'
import path from 'path'

function About({posts}) {
  console.log(posts)
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <h3>{post.filename}</h3>
          <p>{post.content}</p>
        </li>
      ))}
    </ul>
  )
  }
  
export default About

export async function getStaticProps(context) {
  const postsDirectory = path.join(process.cwd(), 'posts')
  const filenames = await fs.readdir(postsDirectory)

  console.log(postsDirectory)
  console.log(filenames)

  const posts = filenames.map(async (filename) => {
    const filePath = path.join(postsDirectory, filename)
    const fileContents = await fs.readFile(filePath, 'utf8')

    // Generally you would parse/transform the contents
    // For example you can transform markdown to HTML here

    return {
      filename,
      content: fileContents,
    }
  })
  // By returning { props: { posts } }, the Blog component
  // will receive `posts` as a prop at build time
  return {
    props: {
      posts: await Promise.all(posts),
    },
  }
}

 

이렇게 만들고 posts폴더를 만든 후 abc.txt 파일을 생성해보자.

 

 

abc.txt

abc

 

실행 해보면 아래 화면과 로그를 확인할 수 있다.

 

 

posts 폴더 하위에 있는 파일들의 이름과 내용을 찾고 About 컴포넌트로 넘겨주었다.

 

 

Write server-side code directly

getStaticProps는 서버사이드에서만 동작한다. 클라이언트 사이드에서는 동작하지 않는다. 또한 브라우저에서 확인 가능한 js bundle에 조차 포함되지 않는다. 이것이 의미하는 것은 direct database queries를 직접 사용해서 정보를 가져올 수 있다는 것이다. getStaticProps로부터 API route를 fetch하면 안된다. 

 

getStaticProps(정적 생성)는 빌드 시 데이터를 가져오고 그곳에서만 처리됩니다. 즉, 프론트엔드와 백엔드가 혼합되지 않습니다. getStaticProps에 데이터가 필요한 경우 데이터를 가져오기 위해 /api에 대한 추가 네트워크 호출을 발생시키지 않아야 합니다. 대신 해당 논리와 함께 함수를 사용하거나 getStaticProps에서 직접 함수를 사용해야 합니다. 프론트엔드에 데이터가 필요한 경우 rest/graphql/whatever/api에서 데이터를 가져오거나 getServerSideProps를 통해 전달할 수 있습니다.

 

아래 예제를 통해서 js bundle에서 무엇이 제거되는지 확인할 수 있다( 브라우저에서 )

// This app shows what Next.js bundles for the client-side with its new SSG
// support. This editor supports TypeScript syntax.
import Cookies from 'cookies';
import Mysql from 'mysql';
import Link from 'next/link';
import SQL from 'sql-template-strings';
import Layout from '../components/Layout';

const pool = Mysql.createPool(process.env.DATABASE_URL);

export default function ({ projects }) {
  return (
    <Layout>
      <h1>Projects</h1>
      <ul>
        {projects.map((project) => (
          <li key={project.id}>
            <Link href="/projects/[id]" as={`/projects/${project.id}`}>
              <a>{project.name}</a>
            </Link>
          </li>
        ))}
      </ul>
    </Layout>
  );
}

export async function getServerSideProps({ req, res }) {
  const userId = new Cookies(req, res).get('user_id');
  const projects = await new Promise((resolve, reject) =>
    pool.query(
      SQL`SELECT id, name FROM projects WHERE user_id = ${userId};`,
      (err, results) => (err ? reject(err) : resolve(results))
    )
  );
  return { props: { projects } };
}
// This is the code that is bundled for the client-side:

import Link from 'next/link';
import Layout from '../components/Layout';
export var __N_SSP = true;
export default function ({ projects }) {
  return (
    <Layout>
      <h1>Projects</h1>
      <ul>
        {projects.map((project) => (
          <li key={project.id}>
            <Link href="/projects/[id]" as={`/projects/${project.id}`}>
              <a>{project.name}</a>
            </Link>
          </li>
        ))}
      </ul>
    </Layout>
  );
}

 

 

Statically Generates both HTML and JSON

getStaticProps를 가지고 있는 페이지가 빌드 시 pre-render 될 때, Next.js는 HTML파일 뿐 아니라 getStaticProps의 실행 결과인 JSON을 생성한다.

 

이것은 클라이언트에서 getStaticProps를 호출하는것이 아니라 단지 export된 JSON을 사용한다는 것을 의미한다.

 

 

Only allowed in a page

getStaticProps는 page에서만 exported 된다. 다른 곳에서는 사용할 수 없다. 이유는 리액트는 페이지가 render되기 전에 data 모두를 가지고 있어야 하기 때문이다. 

 

또한 반드시 export async function getStaticProps() {}로 동작해야한다.

TypeScript쓸 때는 export const getStaticProps: GetStaticProps = async (context) => {}

 

 

Run on every request in development

개발환경에서 getStaticProps는 매 요청시에 호출된다.

 

 

Preview Mode

요청시에 페이지가 render 되길 원하는 경우가 생긴다. 배포되기전에 프리뷰 초안을 보길 원할 수 있다.

자세한 내용은 Preview Mode 문서를 참조하자.

 

 

 

728x90
반응형
728x90
반응형

페이지 내부에서 SSR을 할 때 getStaticProps 를 사용하여 빌드 시 이 페이지를 미리 렌더링한다.

 

pages폴더 하위에 about.tsx 파일을 만들고 다음과 같이 작성해보자.

 

/pages/about.js

function About({data}) {
    console.log(data)
    return <div>About</div>
  }
  
  export default About

  export async function getStaticProps(context) {
    return {
      props: {
          data : "Teepo"
      }, // will be passed to the page component as props
    }
  }

 

아래 getStaticPropspropsdata를 담아서 About 컴포넌트로 보냈다.

 

npm run dev 명령어를 입력하고 확인해보자.

$ npm run dev

 

 

getStaticProps로 전달한 data값이 잘 넘어갔다.

 

나중에 fetch나 axios를 이용할 경우 다음과 같이 구현할 수 있다.

 

about.js

function About({data}) {
    console.log(data)
    return <div>About</div>
  }
  
export default About

export async function getStaticProps(context) {
    const res = await fetch(`https://jsonplaceholder.typicode.c/users`)
    const data = await res.json()

    return {
      props: { data }, // will be passed to the page component as props
    }
}

 

실행화면은 다음과 같다.

 

 

또한 redirect를 반환하여 리디렉션을 할 수 있다.

 

export async function getStaticProps(context) {
    const res = await fetch(`https://jsonplaceholder.typicode.com/users`)
    const data = await res.json()

    if (!data) {
      return {
        redirect: {
          destination: '/',
          permanent: false,
        },
      }
    }
  
    return {
      props: { data }, // will be passed to the page component as props
    }
}

 

다만, 빌드 시 리디렉션은 현재 허용되지 않으며 빌드 시 알려진 경우에 추가해야될 것들이 있다.

자세한 내용은 아래 주소로 확인해보자

 

https://nextjs.org/docs/api-reference/next.config.js/redirects

 

next.config.js: Redirects | Next.js

Add redirects to your Next.js app.

nextjs.org

 

 

언제 getStaticProps를 사용할까?

  • render되는 페이지가 필요한 데이터가 사용자 요청 전 빌드시에 활용가능한 data 일때
  • data가 headless CMS에서 오는 data일때
  • data가 사용자개인화가 필요하지 않은 공공성의 캐시된 data일때
  • 해당 페이지가 SEO를 위해 반드시 pre-render 된 페이지 여야하고 매우 빨리 로드되어야 할 때, -- getStaticProps는 HTML과 JSON파일을 생성하고 둘 다 CDN에 의해 캐시되서 성능적으로 매우 좋다.

 

TypeScript - getStaticProps

타입스크립트를 사용한 about 페이지는 다음과 같다.

 

about.tsx

import { GetStaticProps } from 'next'

interface data  {
    name : string
    age : number
}

function About(data : data) {
    console.log(data)
    return <div>About</div>
  }
  
export default About

export const getStaticProps: GetStaticProps = async (context) => {

    const res = await fetch(`https://jsonplaceholder.typicode.com/users`)
    const data = await res.json()

    if (!data) {
      return {
        redirect: {
          destination: '/',
          permanent: false,
        },
      }
    }
  
    return {
      props: { 
          data: {
              name : "Teepo",
              age : 28
          }
        }, // will be passed to the page component as props
    }
}

GetStaticProps를 import 하고 data를 Object로 만들어보았다.

 

728x90
반응형
728x90
반응형

Next.js의 특징 중 하나는 파일 이름을 기반으로 라우팅이된다.

 

예를들어 다음과같이 코드를 작성하면

 

pages/about.js

function About() {
  return <div>About</div>
}

export default About

이 파일은 /about 페이지가 된다.

 

Pages with Dynamic Routes

Next.js는 동적라우트 할 수 있는 페이지를 제공한다. pages/posts/[id].js 파일을 생성한다면

posts/1, posts/2에 접근할 수 있다.

 

Pre-Rendering

Pre-Rendering은 Next.js에서 중요한 컨셉 중 하나다. 기본적으로 모든 페이지를 pre-rendering한다. 

이는 Next.js가 client-side JavaScript로 작업을 수행하기 전 미리 각 페이지에서 HTML을 만들어 두는 것을 의미한다.

js 기능을 끄더라도 페이지를 볼 수 있다.

 

Pre-Rendering에는 2가지 과정이 있다.

  1. initial load
  2. hydration

 

initial load html

js 동작만 없는 html을 먼저 화면에 보여주는데, 아직 js파일이 로드되기 전 이므로 <Link> 같은 컴포넌트는

동작하지 않는다.

 

hydration

initial load에서 html을 로드한 뒤 js 파일을 서버로부터 받아 html을 연결시키는 과정이다.

여기서 js와 html이 연결된다.

 

 

Pre-Rendering이 없다면?

 

 

JS 전체가 로드되어야 하기 떄문에 최초 Load에서 사용자에게 아무것도 보여지지 않게 된다.

 

Static Generation with data

pre-rendering 시에 외부 data가 필요할 경우 Next.js가 제공하는 특별한 함수를 사용할 수 있는데,

아래 함수 중 한개 혹은 두개 모두 사용이 필요할 수 있다.

 

  1. 만약 페이지 content가 외부 data에 종속된 경우 : getStaticProps를 사용한다.
  2. 페이지 paths가 외부 data에 종속된 경우 : getStaticPaths를 사용한다.(보통getStaticProps도 같이사용한다.)

보통은 Static Generation을 권장한다. 매 요청시마다 페이지를 render하는 SSR보다 훨씬 빠르기 때문이다.

Static Generation은 아래 경우들에 사용할 수 있다.

  • Marketing pages
  • Blog posts
  • E-commece product listings
  • help and documentation

하지만 매 요청에 따라 다른 데이터를 보여주기 위해서는 SSR을 써야한다.

 

 

728x90
반응형
728x90
반응형

참조 

https://nextjs.org/docs/getting-started

 

Getting Started | Next.js

Get started with Next.js in the official documentation, and learn more about all our features!

nextjs.org

 

 

시스템 요구 사항

 - Node.js 12.0 이상

 - MacOS, Windows 및 Linux 지원

 

 

먼저 다음 명령어로 Next.js 앱을 설치한다.

$ npx create-next-app@latest

 

만약 TypeScript로 프로젝트를 실행하려면 다음 명령어를 실행한다.

$ npx create-next-app@latest --typescript

 

 

  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  • dev - 개발 모드에서 Next.js를 시작하는 실행
  • build - 'next build' 프로덕션을 사용
  • start - 'next start' 프로덕션 사용
  • lint - 'next lint' Next.js의 내장 ESLint 구성을 설정
728x90
반응형
728x90
반응형

반복된 코드

export async function getUsers(dispatch) {
  dispatch({ type: 'GET_USERS' });
  try {
    const response = await axios.get(
      'https://jsonplaceholder.typicode.com/users'
    );
    dispatch({ type: 'GET_USERS_SUCCESS', data: response.data });
  } catch (e) {
    dispatch({ type: 'GET_USERS_ERROR', error: e });
  }
}

export async function getUser(dispatch, id) {
  dispatch({ type: 'GET_USER' });
  try {
    const response = await axios.get(
      `https://jsonplaceholder.typicode.com/users/${id}`
    );
    dispatch({ type: 'GET_USER_SUCCESS', data: response.data });
  } catch (e) {
    dispatch({ type: 'GET_USER_ERROR', error: e });
  }
}

 

이 부분을 깔끔하게 리팩토링 해보자. src 디렉토리 하위에 api.js 파일을 만들고 다음과 같이 작성한다.

 

api.js

import axios from 'axios';

export async function getUsers() {
  const response = await axios.get(
    'https://jsonplaceholder.typicode.com/users'
  );
  return response.data;
}

export async function getUser(id) {
  const response = await axios.get(
    `https://jsonplaceholder.typicode.com/users/${id}`
  );
  return response.data;
}

 

그 다음에 src 디렉터리에 asyncActionUtils.js라는 파일을 만들고 코드를 작성한다.

asyncActionUtils.js

// 이 함수는 파라미터로 액션의 타입 (예: GET_USER) 과 Promise 를 만들어주는 함수를 받아옵니다.
export default function createAsyncDispatcher(type, promiseFn) {
  // 성공, 실패에 대한 액션 타입 문자열을 준비합니다.
  const SUCCESS = `${type}_SUCCESS`;
  const ERROR = `${type}_ERROR`;

  // 새로운 함수를 만듭니다.
  // ...rest 를 사용하여 나머지 파라미터를 rest 배열에 담습니다.
  async function actionHandler(dispatch, ...rest) {
    dispatch({ type }); // 요청 시작됨
    try {
      const data = await promiseFn(...rest); // rest 배열을 spread 로 넣어줍니다.
      dispatch({
        type: SUCCESS,
        data
      }); // 성공함
    } catch (e) {
      dispatch({
        type: ERROR,
        error: e
      }); // 실패함
    }
  }

  return actionHandler; // 만든 함수를 반환합니다.
}

 

이렇게 만들어주면 Context 파일이 깔끔해진다.

UserContext.js

import React, { createContext, useReducer, useContext } from 'react';
import createAsyncDispatcher from './createAsyncDispatcher';
import * as api from './api'; // api 파일에서 내보낸 모든 함수들을 불러옴

(...)

export const getUsers = createAsyncDispatcher('GET_USERS', api.getUsers);
export const getUser = createAsyncDispatcher('GET_USER', api.getUser);

 

 

그다음 리듀서쪽 코드도 리팩토링해보자. UsersContextlodingState, success, error를 잘라내서 asyncActionUtils.js 안에 붙여넣고 initialAsyncState 객체와 createAsyncHandler 함수를 만들어서 내보낸다.

 

이제 방금만든 initialAsyncStatecreateAsyncHandler를 사용해서 코드를 고쳐보자.

 

UsersContext.js

import React, { createContext, useReducer, useContext } from 'react';
import {
  createAsyncDispatcher,
  createAsyncHandler,
  initialAsyncState
} from './asyncActionUtils';
import * as api from './api'; // api 파일에서 내보낸 모든 함수들을 불러옴

// UsersContext 에서 사용 할 기본 상태
const initialState = {
  users: initialAsyncState,
  user: initialAsyncState
};

const usersHandler = createAsyncHandler('GET_USERS', 'users');
const userHandler = createAsyncHandler('GET_USER', 'user');

// 위에서 만든 객체 / 유틸 함수들을 사용하여 리듀서 작성
function usersReducer(state, action) {
  switch (action.type) {
    case 'GET_USERS':
    case 'GET_USERS_SUCCESS':
    case 'GET_USERS_ERROR':
      return usersHandler(state, action);
    case 'GET_USER':
    case 'GET_USER_SUCCESS':
    case 'GET_USER_ERROR':
      return userHandler(state, action);
    default:
      throw new Error(`Unhanded action type: ${action.type}`);
  }
}

(...)

 

이렇게 리팩토링을 하면 반복되는 코드를 함수화해서 재사용 할 수 있다.

728x90
반응형
728x90
반응형

src 디렉터리에 UserContext.js 라는 파일을 만들어보자.

 

UsersContext.js

import React, { createContext, useReducer, useContext } from 'react';

// UsersContext 에서 사용 할 기본 상태
const initialState = {
  users: {
    loading: false,
    data: null,
    error: null
  },
  user: {
    loading: false,
    data: null,
    error: null
  }
};

// 로딩중일 때 바뀔 상태 객체
const loadingState = {
  loading: true,
  data: null,
  error: null
};

// 성공했을 때의 상태 만들어주는 함수
const success = data => ({
  loading: false,
  data,
  error: null
});

// 실패했을 때의 상태 만들어주는 함수
const error = error => ({
  loading: false,
  data: null,
  error: error
});

// 위에서 만든 객체 / 유틸 함수들을 사용하여 리듀서 작성
function usersReducer(state, action) {
  switch (action.type) {
    case 'GET_USERS':
      return {
        ...state,
        users: loadingState
      };
    case 'GET_USERS_SUCCESS':
      return {
        ...state,
        users: success(action.data)
      };
    case 'GET_USERS_ERROR':
      return {
        ...state,
        users: error(action.error)
      };
    case 'GET_USER':
      return {
        ...state,
        user: loadingState
      };
    case 'GET_USER_SUCCESS':
      return {
        ...state,
        user: success(action.data)
      };
    case 'GET_USER_ERROR':
      return {
        ...state,
        user: error(action.error)
      };
    default:
      throw new Error(`Unhanded action type: ${action.type}`);
  }
}

// State 용 Context 와 Dispatch 용 Context 따로 만들어주기
const UsersStateContext = createContext(null);
const UsersDispatchContext = createContext(null);

// 위에서 선언한 두가지 Context 들의 Provider 로 감싸주는 컴포넌트
export function UsersProvider({ children }) {
  const [state, dispatch] = useReducer(usersReducer, initialState);
  return (
    <UsersStateContext.Provider value={state}>
      <UsersDispatchContext.Provider value={dispatch}>
        {children}
      </UsersDispatchContext.Provider>
    </UsersStateContext.Provider>
  );
}

// State 를 쉽게 조회 할 수 있게 해주는 커스텀 Hook
export function useUsersState() {
  const state = useContext(UsersStateContext);
  if (!state) {
    throw new Error('Cannot find UsersProvider');
  }
  return state;
}

// Dispatch 를 쉽게 사용 할 수 있게 해주는 커스텀 Hook
export function useUsersDispatch() {
  const dispatch = useContext(UsersDispatchContext);
  if (!dispatch) {
    throw new Error('Cannot find UsersProvider');
  }
  return dispatch;
}

 

만약 id를 가지고 특정 사용자의 정보를 가져오는 API를 호출하고 싶으면 이런 형식으로 해줘야 한다.

dispatch({ type: 'GET_USER' });
try {
  const response = await getUser();
  dispatch({ type: 'GET_USER_SUCCESS', data: response.data });
} catch (e) {
  dispatch({ type: 'GET_USER_ERROR', error: e });
}

 

 

API 처리 함수 만들기

이런 작업들을 처리하는 함수를 만들어보겠다. UserContext.js 파일을 열어서 하단에 함수들을 추가한다.

 

import React, { createContext, useReducer, useContext } from 'react';
import axios from 'axios';

// (...)

export async function getUsers(dispatch) {
  dispatch({ type: 'GET_USERS' });
  try {
    const response = await axios.get(
      'https://jsonplaceholder.typicode.com/users'
    );
    dispatch({ type: 'GET_USERS_SUCCESS', data: response.data });
  } catch (e) {
    dispatch({ type: 'GET_USERS_ERROR', error: e });
  }
}

export async function getUser(dispatch, id) {
  dispatch({ type: 'GET_USER' });
  try {
    const response = await axios.get(
      `https://jsonplaceholder.typicode.com/users/${id}`
    );
    dispatch({ type: 'GET_USER_SUCCESS', data: response.data });
  } catch (e) {
    dispatch({ type: 'GET_USER_ERROR', error: e });
  }
}

 

 

Context 사용하기

이제는 만든 Context를 사용해보자.

 

App.js

import React from 'react';
import Users from './Users';
import { UsersProvider } from './UsersContext';

function App() {
  return (
    <UsersProvider>
      <Users />
    </UsersProvider>
  );
}

export default App;

 

Users.js

import React, { useState } from 'react';
import { useUsersState, useUsersDispatch, getUsers } from './UsersContext';
import User from './User';

function Users() {
  const [userId, setUserId] = useState(null);
  const state = useUsersState();
  const dispatch = useUsersDispatch();

  const { data: users, loading, error } = state.users;
  const fetchData = () => {
    getUsers(dispatch);
  };

  if (loading) return <div>로딩중..</div>;
  if (error) return <div>에러가 발생했습니다</div>;
  if (!users) return <button onClick={fetchData}>불러오기</button>;

  return (
    <>
      <ul>
        {users.map(user => (
          <li
            key={user.id}
            onClick={() => setUserId(user.id)}
            style={{ cursor: 'pointer' }}
          >
            {user.username} ({user.name})
          </li>
        ))}
      </ul>
      <button onClick={fetchData}>다시 불러오기</button>
      {userId && <User id={userId} />}
    </>
  );
}

export default Users;

useUserState로 state를 가져오고 useUserDispatch로 dispatch를 가져온다.

요청을 시작할 때 getUsers 안에 dispatch로 호출해준다.

실행해보면 정상작동 하는것을 볼 수 있다. 이번엔 User를 바꿔보자.

 

User.js

import React, { useEffect } from 'react';
import { useUsersState, useUsersDispatch, getUser } from './UsersContext';

function User({ id }) {
  const state = useUsersState();
  const dispatch = useUsersDispatch();
  useEffect(() => {
    getUser(dispatch, id);
  }, [dispatch, id]);

  const { data: user, loading, error } = state.user;

  if (loading) return <div>로딩중..</div>;
  if (error) return <div>에러가 발생했습니다</div>;
  if (!user) return null;
  return (
    <div>
      <h2>{user.username}</h2>
      <p>
        <b>Email:</b> {user.email}
      </p>
    </div>
  );
}

export default User;

 

useEffect()를 사용해서 id값이 바뀔 때마다 getUser() 함수를 호출해준다. 두번째 파라미터에 props로 받아온 id값을

넣어준다.

 

 

정리

  1. Provider를 만들고 state와 dispatch를 선언하고, userReducer와 initialState를 지정해준다.
  2. state와 dispatch를 Provider로 감싼 내부에서 사용할 수 있도록 Context를 구현한다.
  3. 그 밖에 dispatch를 이용한 함수들을 구현한다.
  4. state의 공유가 필요한 곳을 provider로 감싼다.
  5. 감싸여진 컴포넌트에서 state,dispatch, 함수들을 import 해준다.
  6. 감싸여진 컴포넌트에 할당된 값이 바뀔 때마다 변환을 해주려면 useEffect를 써준다.
728x90
반응형
728x90
반응형

react-async는 우리가 지난 섹션에서 만들었던 useAsync와 비슷한 함수가 들어있는 라이브러리다.

이름은 똑같으나 사용법이 조금 다르다.

 

만약 매번 프로젝트를 만들 때 마다 직접 요청 상태 관리를 위한 커스텀 Hook을 만들기 귀찮으면

이 라이브러리를 사용하면 유용할 것이다.

 

먼저 react-async를 설치한다.

$ npm i react-async

 

import { useAsync } from "react-async"

const loadCustomer = async ({ customerId }, { signal }) => {
  const res = await fetch(`/api/customers/${customerId}`, { signal })
  if (!res.ok) throw new Error(res)
  return res.json()
}

const MyComponent = () => {
  const { data, error, isLoading } = useAsync({ promiseFn: loadCustomer, customerId: 1 })
  if (isLoading) return "Loading..."
  if (error) return `Something went wrong: ${error.message}`
  if (data)
    return (
      <div>
        <strong>Loaded some data:</strong>
        <pre>{JSON.stringify(data, null, 2)}</pre>
      </div>
    )
  return null
}

react-async의 useAsync 를 사용 할 때 파라미터로 넣는 옵션 객체에는 호출 할 함수 promiseFn 을 넣고, 파라미터도 필드 이름과 함께 costomerId를 넣어주어야 한다.

 

User 컴포넌트 전환

User 컴포넌트를 react-async의 useAsync 로 전환하자

import React from 'react';
import axios from 'axios';
import { useAsync } from 'react-async';

async function getUser({ id }) {
  const response = await axios.get(
    `https://jsonplaceholder.typicode.com/users/${id}`
  );
  return response.data;
}

function User({ id }) {
  const { data: user, error, isLoading } = useAsync({
    promiseFn: getUser,
    id,
    watch: id
  });

  if (isLoading) return <div>로딩중..</div>;
  if (error) return <div>에러가 발생했습니다</div>;
  if (!user) return null;

  return (
    <div>
      <h2>{user.username}</h2>
      <p>
        <b>Email:</b> {user.email}
      </p>
    </div>
  );
}

export default User;

 

react-async를 사용할 때에는 프로미스를 반환하는 함수의 파라미터를 객체형태로 해주어야 한다.

async function getUser({ id }) {}

 

그리고 watch 값에 특정 값을 넣어주면 이 값이 바뀔 때마다 promiseFn에 넣은 함수를 다시 호출한다.

 

 

Users 컴포넌트 전환

Users.js

import React, { useState } from 'react';
import axios from 'axios';
import { useAsync } from 'react-async';
import User from './User';

async function getUsers() {
  const response = await axios.get(
    'https://jsonplaceholder.typicode.com/users'
  );
  return response.data;
}

function Users() {
  const [userId, setUserId] = useState(null);
  const { data: users, error, isLoading, reload } = useAsync({
    promiseFn: getUsers
  });

  if (isLoading) return <div>로딩중..</div>;
  if (error) return <div>에러가 발생했습니다</div>;
  if (!users) return <button onClick={reload}>불러오기</button>;
  return (
    <>
      <ul>
        {users.map(user => (
          <li
            key={user.id}
            onClick={() => setUserId(user.id)}
            style={{ cursor: 'pointer' }}
          >
            {user.username} ({user.name})
          </li>
        ))}
      </ul>
      <button onClick={reload}>다시 불러오기</button>
      {userId && <User id={userId} />}
    </>
  );
}

export default Users;

 

 

이전에 사용했던 skip처럼 나중에 데이터를 불러오려면 promiseFn 대신 deferFn, reload 대신 run 함수를 사용한다.

 

Users.js

import React, { useState } from 'react';
import axios from 'axios';
import { useAsync } from 'react-async';
import User from './User';

async function getUsers() {
  const response = await axios.get(
    'https://jsonplaceholder.typicode.com/users'
  );
  return response.data;
}

function Users() {
  const [userId, setUserId] = useState(null);
  const { data: users, error, isLoading, run } = useAsync({
    deferFn: getUsers
  });

  if (isLoading) return <div>로딩중..</div>;
  if (error) return <div>에러가 발생했습니다</div>;
  if (!users) return <button onClick={run}>불러오기</button>;

  return (
    <>
      <ul>
        {users.map(user => (
          <li
            key={user.id}
            onClick={() => setUserId(user.id)}
            style={{ cursor: 'pointer' }}
          >
            {user.username} ({user.name})
          </li>
        ))}
      </ul>
      <button onClick={run}>다시 불러오기</button>
      {userId && <User id={userId} />}
    </>
  );
}

export default Users;

 

 

정리 

  • 로그인 할 때는 최상위 컴포넌트에 쿠키를 state에 담고 상태를 관리해준다.(로그아웃, 프로필 수정)
  • 로그아웃 했을 경우(state가 null값이 될 때) 최상위에서 첫 화면 컴포넌트를 return 한다.
  • API가 필요한 경우 처음 페이지가 렌더링 될 때 useAsync로 state(리스트, 사진 등)를 만들어준다.
  • API 필요 없을 경우 처음 페이지 렌더링 될 때 useEffect를 사용해준다.
  • 추가, 수정, 삭제가 필요한 데이터의 경우 reducer를 만들어서 관리한다.(action으로 실행)
728x90
반응형
728x90
반응형

데이터를 요청해야 할 때마다 리듀서를 작성하는 것은 매우 번거롭다.

커스텀 Hook을 만들어서 요청 상태 관리 로직을 쉽게 재사용하는 방법을 알아보자.

src폴더 하위에 useAsync.js 파일을 만든다.

 

useAsync.js

import { useReducer, useEffect } from 'react';

function reducer(state, action) {
  switch (action.type) {
    case 'LOADING':
      return {
        loading: true,
        data: null,
        error: null
      };
    case 'SUCCESS':
      return {
        loading: false,
        data: action.data,
        error: null
      };
    case 'ERROR':
      return {
        loading: false,
        data: null,
        error: action.error
      };
    default:
      throw new Error(`Unhandled action type: ${action.type}`);
  }
}

function useAsync(callback, deps = []) {
  const [state, dispatch] = useReducer(reducer, {
    loading: false,
    data: null,
    error: false
  });

  const fetchData = async () => {
    dispatch({ type: 'LOADING' });
    try {
      const data = await callback();
      dispatch({ type: 'SUCCESS', data });
    } catch (e) {
      dispatch({ type: 'ERROR', error: e });
    }
  };

  useEffect(() => {
    fetchData();
    // eslint 설정을 다음 줄에서만 비활성화
    // eslint-disable-next-line
  }, deps);

  return [state, fetchData];
}

export default useAsync;

 

useAsync 함수는 두가지 파라미터를 받아온다. 첫 번째 파라미터는 API 요청을 시작하는 함수이고, 두 번째 파라미터는 deps 인데 이 deps 값은 해당 함수 안에서 사용하는 useEffect 의 deps 로 설정된다.

 

나중에 우리가 사용 할 비동기 함수에서 파라미터가 필요하고, 그 파라미터가 바뀔 때 새로운 데이터를 불러오고 싶을 경우에 활욜 할 수 있다.

 

이제 이 Hook을 써보자.

 

Users.js

import React from 'react';
import axios from 'axios';
import useAsync from './useAsync';

// useAsync 에서는 Promise 의 결과를 바로 data 에 담기 때문에,
// 요청을 한 이후 response 에서 data 추출하여 반환하는 함수를 따로 만들었습니다.
async function getUsers() {
  const response = await axios.get(
    'https://jsonplaceholder.typicode.com/users'
  );
  return response.data;
}

function Users() {
  const [state, refetch] = useAsync(getUsers, []);

  const { loading, data: users, error } = state; // state.data 를 users 키워드로 조회

  if (loading) return <div>로딩중..</div>;
  if (error) return <div>에러가 발생했습니다</div>;
  if (!users) return null;
  return (
    <>
      <ul>
        {users.map(user => (
          <li key={user.id}>
            {user.username} ({user.name})
          </li>
        ))}
      </ul>
      <button onClick={refetch}>다시 불러오기</button>
    </>
  );
}

export default Users;

  

버튼 클릭 시 불러오기

만약 특정 버튼을 눌렀을 경우에만 API를 요청하고 싶다면 이렇게 하면 된다.

useAsync.js

import { useReducer, useEffect } from 'react';

function reducer(state, action) {
  switch (action.type) {
    case 'LOADING':
      return {
        loading: true,
        data: null,
        error: null
      };
    case 'SUCCESS':
      return {
        loading: false,
        data: action.data,
        error: null
      };
    case 'ERROR':
      return {
        loading: false,
        data: null,
        error: action.error
      };
    default:
      throw new Error(`Unhandled action type: ${action.type}`);
  }
}

function useAsync(callback, deps = [], skip = false) {
  const [state, dispatch] = useReducer(reducer, {
    loading: false,
    data: null,
    error: false
  });

  const fetchData = async () => {
    dispatch({ type: 'LOADING' });
    try {
      const data = await callback();
      dispatch({ type: 'SUCCESS', data });
    } catch (e) {
      dispatch({ type: 'ERROR', error: e });
    }
  };

  useEffect(() => {
    if (skip) return;
    fetchData();
    // eslint 설정을 다음 줄에서만 비활성화
    // eslint-disable-next-line
  }, deps);

  return [state, fetchData];
}

export default useAsync;

skip 파라미터의 기본 값을 false로 지정하고, 만약 이 값이 true라면 useEffect 에서 아무런 작업도 하지 않도록 설정해주었다.

 

Users.js

import React from 'react';
import axios from 'axios';
import useAsync from './useAsync';

// useAsync 에서는 Promise 의 결과를 바로 data 에 담기 때문에,
// 요청을 한 이후 response 에서 data 추출하여 반환하는 함수를 따로 만들었습니다.
async function getUsers() {
  const response = await axios.get(
    'https://jsonplaceholder.typicode.com/users'
  );
  return response.data;
}

function Users() {
  const [state, refetch] = useAsync(getUsers, [], true);

  const { loading, data: users, error } = state; // state.data 를 users 키워드로 조회

  if (loading) return <div>로딩중..</div>;
  if (error) return <div>에러가 발생했습니다</div>;
  if (!users) return <button onClick={refetch}>불러오기</button>;
  return (
    <>
      <ul>
        {users.map(user => (
          <li key={user.id}>
            {user.username} ({user.name})
          </li>
        ))}
      </ul>
      <button onClick={refetch}>다시 불러오기</button>
    </>
  );
}

export default Users;

useAsync 의 세 번째 파라미터에 true를 넣어줬고, !users인 상황에 불러오기 버튼을 렌더링해주었다.

 

 

API에 파라미터가 필요한 경우

이번에는 API를 요청 할 때 파라미터가 필요한 경우에 어떻게 해야 하는지 알아보겠다.

 

User 라는 컴포넌트를 만들고, id값을 props로 받아와서

 

https://jsonplaceholder.typicode.com/users/1

이런 식으로 맨 뒤에  id를 넣어서 API를 요청할 것이다.

src 폴더에 User.js 를 생성 후 다음 코드를 작성해보자.

 

User.js

import React from 'react';
import axios from 'axios';
import useAsync from './useAsync';

async function getUser(id) {
  const response = await axios.get(
    `https://jsonplaceholder.typicode.com/users/${id}`
  );
  return response.data;
}

function User({ id }) {
  const [state] = useAsync(() => getUser(id), [id]);
  const { loading, data: user, error } = state;

  if (loading) return <div>로딩중..</div>;
  if (error) return <div>에러가 발생했습니다</div>;
  if (!user) return null;

  return (
    <div>
      <h2>{user.username}</h2>
      <p>
        <b>Email:</b> {user.email}
      </p>
    </div>
  );
}

export default User;

useAsync 를 사용할 때 파라미터를 포함시켜서 함수를 호출하는 새로운 함수를 만들어서 등록한다.

그리고 id가 바뀔 때마다 재호출 되도록 deps에 id를 추가한다.

 

그 다음 Users.js에서 useState를 사용하여 userId의 상태를 관리한다. 초깃값은 null이며 리스트에 항목을

클릭할 때마다 사용자의 id를 userId 값으로 설정해준다.

 

Users.js

import React, { useState } from 'react';
import axios from 'axios';
import useAsync from './useAsync';
import User from './User';

// useAsync 에서는 Promise 의 결과를 바로 data 에 담기 때문에,
// 요청을 한 이후 response 에서 data 추출하여 반환하는 함수를 따로 만들었습니다.
async function getUsers() {
  const response = await axios.get(
    'https://jsonplaceholder.typicode.com/users'
  );
  return response.data;
}

function Users() {
  const [userId, setUserId] = useState(null);
  const [state, refetch] = useAsync(getUsers, [], true);

  const { loading, data: users, error } = state; // state.data 를 users 키워드로 조회

  if (loading) return <div>로딩중..</div>;
  if (error) return <div>에러가 발생했습니다</div>;
  if (!users) return <button onClick={refetch}>불러오기</button>;
  return (
    <>
      <ul>
        {users.map(user => (
          <li
            key={user.id}
            onClick={() => setUserId(user.id)}
            style={{ cursor: 'pointer' }}
          >
            {user.username} ({user.name})
          </li>
        ))}
      </ul>
      <button onClick={refetch}>다시 불러오기</button>
      {userId && <User id={userId} />}
    </>
  );
}

export default Users;

 

실행해보면

 

 

id를 클릭할 때마다 useState로 선언된 userId가 바뀌게 되고, User 컴포넌트에 userId가 할당되면서 렌더리이 되는 것을

확인할 수 있다.

728x90
반응형

+ Recent posts