Data Fetching

Data Fetching

Next.js 에서는 어플리케이션의 사용법에 따라 다양한 방식의 데이터 수집 방법을 이용할 수 있다.

데이터를 가져오는 방법으로는 크게 아래 5가지 방법이 있다.

  • SSR (Server-side rendering) - getServerSideProps
  • SSG (Static-site generation) - getStaticProps
  • CSR (Client-side rendering)
  • ISR (Incremental Static Regeneration)
  • Dynamic Routing - getStaticPaths

getServerSideProps

next의 SSR에서는 데이터를 가져오기 위해 getServerSideProps를 이용한다.

이는 서버에 요청을 보냈을 때 요청단계에서 이미 필요한 데이터를 수집하기 위해 사용된다.

요청 단계에서 꼭 필요하지 않고 브라우저에서 수집해도 되는 데이터라면, 브라우저에서 수집하거나, getStaticProps를 사용하는 것이 좋다.

getServerSidePropsexport하면 Next는 getServerSideProps에서 리턴되는 데이터를 미리 렌더링 한다.

getServerSideProps를 통해 props를 넘길 때에는 클라이언트에 전달되므로 민감한 정보를 포함하면 안된다.

이 함수는 서버사이드에서 단 한번만 실행되며 브라우저에서 절대 실행되지 않는다.

getServerSideProps는 아래와 같이 사용하며, return 해주는 props가 실제로 페이지에 전달된다.

페이지에서만 사용할 수 있으며, 이외의 파일에서는 내보낼 수 없다.

export async function getServerSideProps(context) {
    return {
        props: {},
    }
}

getServerSideProps서버에서만 실행되며 브라우저에서는 실행되지 않으며 동작은 아래와 같다.

페이지가 최초 요청인 경우(SSR) getServerSideProps에서 리턴된 props를 통해 pre-rendered 된 결과를 보내준다.

페이지가 next/linknext/router를 통해 이동된 경우 next는 서버에 요청을 보내게 되고, 서버에서 getServerSideProps를 처리한다.

getServerSideProps는 항상 독립적인 함수여야하며 페이지의 컴포넌트로서 동작하면 안된다.

getServerSideProps와 API Route 사용

getServerSideProps에서 API Route의 요청이 필요하다면, API Route로 요청하지 말고, 해당 API Route의 로직을 직접 가져온다.

API Route로 요청하게 되면 불필요한 요청이 늘어나 성능을 저하시킨다.

page/api/~

자주 업데이트 되는 데이터가 있다면?

자주 업데이트 되는 데이터가 있다면, 해당 부분은 클라이언트에서 가져오는 것이 좋다.

예를 들어 대시보드의 데이터 같은 경우 사용자의 개인적인 정보 이므로 SEO가 필요하지 않다. 이런 경우 해당 데이터는 클라이언트 단계에서 수집할 수 있다.

getServerSideProps로 데이터 가져오기

// 아래 함수를 가지는 페이지는 data라는 props을 받을 수 있다.
export async function getServerSideProps(){
    const { data } = await api.get(`https://.../`)
    return {
        props: {
            data,
        }
    }
}

getServerSideProps에서 오류가 발생한 경우

getServerSideProps에서 오류가 발생하면, next는 pages/500.js를 보여준다.

getServerSideProps 사용 예시

route 데이터 사용하기

nextjs에서 useRouter를 사용하여 asPath를 이용하려는 경우, SSR 과 CSR의 asPath 최초 값이 다르게 출력된다.

예를 들어 slug를 이용하여 /pages/parent/[id].tsx를 접근한다고 가정해보자.

const Page = () => {
    const { query } = useRouter() 
    console.log(query)
    
    return <></>
}

위 소스에서 /parent/test로 접근할 때

SSR에서는 {}{id: 'test'}가 출력되고

CSR에서는 {id: 'test'}만 출력된다.

SSR에서는 slug에 따라 값이 변하는 것인데, slug를 사용해야하는 경우 아래처럼 해결이 가능하다.

export function getServerSideProps(context){
    return {
        props: {
            id: context.query.id
        }
    }
}



// SSR-CSR
const Page = ({id}) => {
    console.log(id)
    
    return <></>
}

위 소스에서는 SSR과 CSR 모두 test라는 결과를 출력한다.

아직 정확한 이유는 모르겠지만, 사실 위처럼 하지 않고, getServerSideProps 함수를 export 하기만 해도 useRouter의 값은 한번만 전달된다.

이는 next에서 정적으로 최적화된 페이지는 route parameter가 없이 전달된 후 hydration을 거치며 query 정보가 채워지는데

getServerSideProps를 제공하면 context에서 이미 resolveUrl을 갖고 있기 때문에 채워서 내려주는 것 같다.

자세한 내용은 좀 더 알아볼 필요가 있다.

API 요청 데이터 사용하기

api를 통해서 데이터를 가져오고 props로 받고 싶다면 다음과 같이 구현할 수 있다.

export async function getServerSideProps(){
    const { data } = await api.get(`https://.../`)
    return {
        props: {
            data,
        }
    }
}

const Page = ({data}) => {
    return (
        <ul>
        	{data.map((item) => <li key={item}>{item}</li>)}
    	</ul>
    )
}

react-query와 사용하기

위의 예제에서 react-query와 함께 사용하려면 아래처럼 사용할 수 있다.

async function getData(){
    const { data } = await api.get(`https://.../`)
    return data
}

export async function getServerSideProps(){
    const data = await getData()
    return {
        props: {
            data
        }
    }
}

const Page = ({data:initialData}) {
    const { data } = useQuery(['data'], getData, { initialData })
    
    //...
}

getStaticProps

next의 SSG에서는 데이터를 미리 가져오기 위해 getStaticProps를 사용한다.

이는 빌드 단계에서 데이터를 수집하는 경우 사용된다.

getStaticPropsHTML이나 JSON을 생성하며 이는 CDN에 의해 캐시될 수 있고, 매우 빠르게 제공될 수 있다.

getStaticPropsexport 하면 Next는 getStaticProps에서 리턴되는 데이터를 미리 렌더링 한다.

getStaticProps 역시 민감한 데이터를 props를 통해 넘기면 안된다.

이 함수는 빌드타임에만 실행되며 이후로는 실행되지 않는다.

getStaticProps는 아래와 같이 사용하며, return 해주는 props가 실제 페이지에 전달된다.

이 역시 페이지에서만 사용할 수 있으며, 이외의 파일에선 동작하지 않는다.

export async function getStaticProps(context) {
  return {
    props: {},
  }
}

getStaticProps의 동작은 아래와 같다.

  • getStaticPropsnext build인 동안에만 실행된다.
  • getStaticPropsfallback:true가 포함되면 백그라운드에서 실행된다. *
  • getStaticPropsfallback:blocking을 사용하면 최초 렌더 이전에 실행된다. *
  • getStaticPropsrevalidate를 사용하면 백그라운드에서 실행된다.

getStaticProps는 빌드타임에만 실행되므로 getServerSideProps처럼 요청에 대한 엑세스는 할 수 없다.

요청에 접근해야하는 경우 Middleware 사용을 고려하는 것이 좋다.

getStaticProps역시 독립적인 함수여야 하며 오직 페이지에서만 내보낼 수 있다. _app, _document, _error에서는 사용할 수 없다.

getStaticProps로 데이터 가져오기

// 아래 data는 페이지에서 props로 받을 수 있다.
export async function getStaticProps(){
    const { data } = await api.get(`https://.../`)
    return {
        props: {
            data,
        }
    }
}

getStaticProps 데이터 갱신

getStaticProps의 경우 기본적으로 빌드타임 한번만 실행된다.

따라서 위의 데이터 수집을 빌드타임 한번에만 실행되며 이후 매번 새로 요청하지 않으므로, 사용자는 캐싱된 데이터만 보게 된다.

이때 앞서 말한 revalidate를 통하여 정적 페이지를 업데이트 할 수 있다.

export async function getStaticProps(){
    const { data } = await api.get(`https://.../`)
    return {
        props: {
            data,
        },
        revalidate: 60 // 60초 마다 데이터 변경 여부를 확인한다.
    }
}

이는 Incremental Static Regeneration ISR 이라고 한다.

Development 환경에서의 동작

next dev에 의한 동작에서는 getStaticProps는 매 요청마다 실행된다.

2주차

getStaticPaths

next에서 동적 라우팅을 사용할 때, 미리 정적으로 빌드할 경로들을 지정하기 위해 getStaticPaths를 사용한다.

이는 빌드 타임에서 실행되며, 런타임에서는 호출되지 않는다.

getStaticPaths를 사용하는 경우 getStaticProps 사용이 필요하다.

단, getStaticPathsgetServerSideProps와 함께 사용할 수 없다.

getStaticPathsHTML이나 JSON을 생성하며 이는 CDN에 의해 캐시될 수 있고, 매우 빠르게 제공될 수 있다.

getStaticPaths는 아래와 같이 사용하며, 리턴해주는 paths에 대해서 정적 페이지를 생성한다.

export async function getStaticPaths(){
    return {
        paths: [
            {
                params: { id: '1', },
            },
            {
                params: { id: '2', },
            }
        ],
        //fallback: [boolean | 'blocking']
    }
}

export async function getStaticProps(context){
    return {
        props: {
        	//...
        }
    }
}

getStaticPaths는 주로 데이터베이스, 헤드리스 CMS, 파일 시스템 등에서 데이터를 가져오며 공개적으로 캐시할 수 있을 때 사용한다.

getStaticPaths 역시 독립적인 함수여야 하며, 오직 페이지에서만 내보낼 수 있다.

getStaticPaths 데이터 가져오기

export async function getStaticPaths(){
    return {
        paths: [
            {
                params: { id: '1', },
            },
            {
                params: { id: '2', },
            }
        ],
        fallback: false,
    }
}

// id: '1', id: '2' 각각 호출됨
export async function getStaticProps({params}){
    const { data } = await api.get(`https://.../${params.id}`)
    return {
        props: {
        	data,
        }
    }
}

// /pages/post/[id]
// /pages/post/1
// /pages/post/2

getStaticPaths에서 선택적 페이지 생성하기

일반적으로 production 빌드를 제외하고 빌드할 때 페이지가 수천개가 넘는다면 빌드가 너무 오래걸릴 수 있다.

이럴때에는 빈 paths를 내보내서 페이지 생성을 건너 뛰거나 특정 paths만을 리턴해서 빌드 시간을 단축할 수 있다.

export async function getStaticPaths(){
    if(process.env.SKIP_BUILD_STATIC_GENERATION){
        return {
            paths: []
        }
    }
    
    return {
        paths: [
            //...
        ],
        fallback: false
    }
}

paths

paths는 기본적으로 params를 가집니다.

동적 라우팅에 따라 주소에서 받는 파라미터를 params가 포함해야 합니다.

// ex)

// "/post/[id]"
params: {
    id: ''
}

// "/post/[postId]/[commentId]"
params: {
    postId: '',
    commentId: ''
}

// "/post/[...slug]"
params: {
    slug: [
        '',
        //...
    ]
}

fallback

fallback: false

fallback이 false이면 getStaticPaths에서 리턴되지 않는 경로는 404 페이지가 출력됩니다.

fallback: true

fallback이 true이면 getStaticPaths에서 리턴되지 않는 경로는 404 페이지가 출력되지 않고 대체 페이지가 출력됩니다.

//Post = page
function Post({post}){
    const router = useRouter()
    
    if(router.isFallback){
        return <div>loading...</div>
    }
    
    return <div>Post...</div>
}

fallback 여부는 router.isFallback를 통해 알 수 있습니다.

단, 구글 같은 크롤러에서 또는, next/linknext/router를 통한 이동에서는 'blocking' 처럼 동작합니다.

fallback: truenext export 에서는 사용할 수 없습니다.

fallback: true는 정적 페이지가 많은 경우 유리합니다.

일부 페이지만 빌드 타임에 생성하고 이후에는 요청이 들어올 때 서버에서 getStaticProps를 호출하여 그 결과를 통해 페이지를 캐싱하고 리턴합니다.

fallback : 'blocking'

fallback이 'blocking'이면 getStaticPaths에서 리턴되지 않는 경로는 404 페이지가 출력되지 않고, 페이지가 생성될 때까지 기다립니다.

fallback: true와 다른 점은 true의 경우 로딩 페이지를 출력하고, 'blocking'의 경우 서버에서 HTML을 내려줄 때까지 기다립니다.

따라서 loading/fallback에 의한 flash가 없습니다.

Client-side data fetching

페이지가 SEO 최적화가 필요하지 않은 경우 클라이언트에서 데이터를 가져오는 것이 효율적입니다.

페이지가 아닌 컴포넌트 단위에서 데이터를 가져올 수 있으며, 주로 자주 업데이트 되는 데이터를 가져옵니다.

단, 클라이언트에서 데이터를 가져오기 때문에 어플리케이션의 성능과 페이지 로드 속도에 영향을 미칠 수 있습니다.

client-side에서 데이터 가져오기

function Profile() {
    const [data, setData] = useState<IProfileData>(null)
    const [isLoading, setLoading] = useState<boolean>(false)
    
    useEffect(() => {
        setLoading(true)
        api.get(`https://.../`).then(({data}) => {
            setData(data)
            setLoading(false)
        })
    }, [])
    
    // ...
}

SWR 사용하여 데이터 가져오기

const fetcher = (...args) => api.get(...args)

function Profile() {
    const {data, error} = useSWR(`https://.../`, fetcher)
    
    
    // ...
}

React-query 사용하여 데이터 가져오기

const getProfile = () => api.get(`https://.../`)

export async function getStaticProps(){
    return {
        props: {
            data: [
                {
                    // ...
                }
            ]
        }
    }
}

// react-query + ssr
function Profile(props){
    const {data} = useQuery(['profile'], getProfile, {initialData: props.data})
    
    // ...
}

Incremental Static Regeneration

Next에서는 빌드가 끝난 이후에도 정적 페이지를 생성하거나 수정할 수 있습니다.

ISR은 전체 페이지를 다시 빌드하지 않고 페이지 단위로 정적 생성을 가능하게 합니다.

ISR을 사용하면 수 많은 페이지들을 자유롭게 확장할 수 있습니다.

ISR을 사용하기 위해서는 getStaticPropsrevalidate를 넣습니다.

// 이 함수는 빌드타임에서 한 번 실행된 후, revalidation 상태이면 새 요청 발생 시 실행됩니다.
export async function getStaticProps(){
    return {
        props: {
            // ...
        },
        // 요청이 들어올 때마다 많아도 10초에 1번 실행됩니다.
        revalidate: 10,
    }
}

// 이 함수는 빌드타임에서 한 번 실행된 후, 생성되지 않은 페이지가 요청되면 실행됩니다.
export async function getStaticPaths(){
    return {
        paths: [
            // ...
        ],
        fallback: 'blocking'
    }
}

선택적 Revalidation

기본적인 revalidation은 revalidate 시간에 따라 해당 시간 동안은 동일한 페이지를 보게됩니다.

이후 누군가가 페이지를 방문하게 되면 그때 변동이 있다면 캐시를 무효화하고 업데이트를 하게 됩니다.

v12.2.0 부터 선택적 revalidation을 지원하여 수동으로 업데이트를 할 수 있습니다.

선택적 revalidation 에서는 revalidate 속성을 생략합니다.

대신 res.revalidate('path') 를 통해 수동으로 업데이트 할 수 있습니다.

// pages/api/revalidate.js

export default async function handler(req, res){
    if(req.query.secret !== process.env.MY_SECRET_TOKEN) {
        return res.status(401).json({message: 'invalid token'})
    }
    
    try{
        // res.revalidate('/path-to-revalidate')
        res.revalidate(req.query.path)
        return res.json({revalidated: true})
    } catch(err) {
        return res.status(500).send('Error revalidating')
    }
}

셀프 호스팅 ISR

쿠버네티스 같은 환경에서 사용시 각 pod은 in-memory 캐시를 사용합니다.

이는 각 pod에 요청이 닿을 때 까지 구형 캐시를 사용할 수 있습니다.

이를 해결하기 위해 in-memory 캐시를 비활성화하여 각 pod이 최신 데이터를 참조하도록 할 수 있습니다.

// next.config.js
module.exports = {
    experimental: {
        isrMemoryCacheSize: 0
    }
}

getInitialProps

기존에 next에서 ssr을 구현하기 위해 getInitialProps를 사용했지만, 최신 버전에서는 getStaticProps 또는 getServerSideProps 사용을 권장합니다.


Written by@[esllo]
plain developer

GitHubTwitterLinkedIn