라이북러리를 Next.js로 작업하고 있지만 리액트 훅이 필요한 컴포넌트가 많아서 주로 클라이언트 컴포넌트로 작업하고 있다.
하지만 클라이언트 컴포넌트가 갖고 있는 단점이 있는데, 바로 느린 페이지 로딩 속도이다. HTML을 로드하고 리액트 문법을 적용하기 위해 브라우저가 HTML을 분석하는 과정인 Hydration 때문에 느려지는 것이다.
🔴 문제 살펴보기
독서 상세 페이지로 이동하면 로딩하는 동안 빈 데이터가 나타나기 때문에 흐름이 끊기는 느낌이 든다.
코드로 자세히 살펴보자.
// app/book/[id]/page.tsx
function page({ params }: { params: { id: string } }) {
return <BookPage id={params.id} />
}
export default page
// components/book/BookPage.tsx
'use client'
function BookPage({ id }: BookPageProps) {
const [title, setTitle] = useState('')
const [cover, setCover] = useState('')
...
useEffect(
function fetchBookInfo() {
;(async () => {
if (id) {
const info = await getBookInfo({ isbn: id })
setTitle(info?.title || '')
setCover(info?.cover || '')
...
}
})()
},
[id]
)
return (
<>
<PageTitle route={title} />
<ReadingInfo id={id} title={title} cover={cover} />
...
</>
)
}
export default BookPage
Next.js app router에서는 서버 컴포넌트만 다루기 때문에 BookPage
컴포넌트로 따로 분리하였다. id
값에 따른 독서 정보를 불러오기 위해 BookPage
에 id
props를 내려주고 fetchBookInfo
비동기 함수를 실행시켜 필요한 독서 정보 상태를 업데이트한다.
로딩하는 동안 빈 데이터가 화면에 뜨는 이유가 id
props가 중간에 들어오기 때문이다.
그렇다면 서버 컴포넌트에서 독서 정보를 불러와서 처리하는 방법이 없을까?
🔵 해결 방법
서버 컴포넌트에서도 비동기 함수로 값을 가져올 수 있다!
// app/book/[id]/page.tsx
async function page({ params }: { params: { id: string } }) {
const { id } = params
const info = await getBookInfo({ isbn: id })
return (
<>
<PageTitle route={info.title} />
<ReadingInfo id={id} title={info.title} cover={info.cover} />
<BookInfo
author={info.author}
publisher={info.publisher}
pubdate={info.pubdate}
desc={info.desc}
category={info.category}
isbn={id}
price={info.price}
/>
</>
)
}
export default page
함수 앞에 async
를 붙여주고 데이터를 불러오는데 필요한 getBookInfo
비동기 함수를 사용한다. 이렇게 하면 더이상 useState와 useEffect 훅을 사용해 데이터를 관리했던 BookPage
컴포넌트가 필요없다.
페이지 로딩과 데이터 패칭이 더욱 빨라진 모습이다.
🟢 사용자 경험 개선
여기서 끝이 아니다. 서버 컴포넌트도 초기에는 데이터를 불러오기까지 시간이 요소된다. 사용자 입장에서는 잠깐의 의미 없는 시간 지연이라고 여길 수 있다.
이것을 해결하기 위해 로딩 화면을 중간에 띄울 수 있다.
// app/book/[id]/page.tsx
async function page({ params }: { params: { id: string } }) {
const { id } = params
const info = await getBookInfo({ isbn: id })
return (
<Suspense fallback={<div style={{ background: 'skyblue' }}>Loading</div>}>
<PageTitle route={info.title} />
<ReadingInfo id={id} title={info.title} cover={info.cover} />
<BookInfo
author={info.author}
publisher={info.publisher}
...
/>
<BookPage id={id} />
</Suspense>
)
}
export default page
리액트의 Suspense를 사용하면 스켈레톤과 같은 로딩 표시를 pre-rendering 할 수 있다.
순식간이지만 데이터가 느린 환경에서는 반드시 필요한 작업이다.
좀 더 자연스러운 로딩 표시를 위해 스켈레톤 스타일을 만들어 적용하였다.
이제 사용자가 로딩하며 겪는 지루함을 없앨 수 있게 되었다!
Loading UI and Streaming https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming