Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Next.js のデータフェッチについて

7223a5ca8ff3affa000f39ea66b1b604?s=47 aku11i
November 24, 2021

Next.js のデータフェッチについて

2021/11/24 に開催された【Next.js Update!】v12リリースを踏まえ、Next.jsの採用を考える( https://forkwell.connpass.com/event/228457/ )での事例講演で使用したスライドです。
YouTube: https://www.youtube.com/watch?v=KaS3bgz_CA4

前セッション:Web Frontend Unit の立ち上げとリアーキテクチャに際してNext.js を採用したワケ
https://www.slideshare.net/IidaYukako/nextjs-update-20211124-web-frontend-unit-nextjs

7223a5ca8ff3affa000f39ea66b1b604?s=128

aku11i

November 24, 2021
Tweet

Other Decks in Programming

Transcript

  1. Next.js のデータフェッチについて 2021.11.24 【Next.js Update!】v12リリースを踏まえ、Next.jsの採用を考える 株式会社 AlphaDrive Shuhei Akutagawa /

    芥川 周平
  2. 2 自己紹介 芥川 周平 2017.04 - 受託開発の会社でWeb系の新規開発を4つほど経験 2021.09 - 株式会社アルファドライブ

    / 株式会社ニューズピックス に所属 Web Frontend Unit で NewsPicks のリアーキテクチャを行っている 兵庫県姫路市🏯からフルリモート @aku11i
  3. 3 こちらのセッションの続きです! Web Frontend Unit の立ち上げとリアーキ テクチャに際してNext.js を採用したワケ https://www.slideshare.net/IidaYukako/nextjs-upda te-20211124-web-frontend-unit-nextjs

  4. 4 本日話すこと • getInitialProps を使用している話 • データフェッチ処理を再利用したい話

  5. 5 getInitialProps を使用している話

  6. 6 NewsPicks のリアーキテクチャについて NewsPicks ではフロントエンドのリアーキテクチャを行っている • モノリスな Web 基盤からフロントエンド部分を剥がし、 Next.js

    に載せ替えている • 初期段階ではページの見た目や機能はそのままに基盤だけを載せ換える方針 • 新基盤でも画面挙動を合わせたいので レンダリング前に必要なデータを全て取得しておく必要がある → getInitialProps or getServerSideProps を活用する
  7. 7 現行基盤 Webサーバー DB リクエストの度に HTML を構築して返却 新基盤 BFF getInitialProps

    or getServerSideProps NewsPicks のリアーキテクチャについて
  8. 8 現行基盤 Webサーバー DB リクエストの度に HTML を構築して返却 新基盤 BFF getInitialProps

    or getServerSideProps NewsPicks のリアーキテクチャについて getInitialProps を採用
  9. 9 • 初めは getServerSideProps を使用していた • どのページでも必要になる共通データの BFF へのリクエストを抑えたい話が出た ◦

    ヘッダーに表示するユーザー情報など • Apollo Client で GraphQL のクエリ結果をキャッシュするのがシンプルっぽい 👀 ◦ getServerSideProps では実現できなかった(詳細は次ページで解説) → getInitialProps を採用することに なぜ getInitialProps ?
  10. 10 データフェッチと Apollo Client のキャッシュ について BFF BFF getInitialProps (初回)

    getServerSideProps getInitialProps (2回目以降)
  11. 11 BFF BFF getInitialProps (初回) getServerSideProps getInitialProps (2回目以降) 様々なユーザーのクエリを実行する のでキャッシュを利用できない

    データフェッチと Apollo Client のキャッシュ について
  12. 12 キャッシュを利用できる → 一度実行したクエリは キャッシュからデータを取り出せる BFF BFF getInitialProps (初回) getServerSideProps

    getInitialProps (2回目以降) 様々なユーザーのクエリを実行する のでキャッシュを利用できない データフェッチと Apollo Client のキャッシュ について
  13. 13 • 私の認識: ◦ getInitialProps はサーバーとクライアントの両方で実行され、 ▪ Node.js と Browser

    に依存しないコードが求められる ▪ クライアント向けのバンドルにデータフェッチ部分のコードが混入する ◦ といった管理の難しさがある ◦ 上記を解決するためにより良い選択肢として getServerSideProps / getStaticProps が登場 • 現行基盤の画面挙動と合わせつつキャッシュを使うために getInitialProps を選択 • 新規に設計をする場合はまず getServerSideProps / getStaticProps の使用を検討 getInitialProps の立ち位置 非推奨ではない getInitialProps を推奨するわけではないです。 非推奨ではないのでまだ使えるという話です!
  14. 14 データフェッチ処理を再利用したい話

  15. 15 getInitialProps (getServerSideProps) 、膨らみがち🔥 同じような処理が複数ページに散らばりがち  → 複数ファイルに変更が及びがち データフェッチ処理を再利用したい

  16. 16 getInitialProps (getServerSideProps) 、膨らみがち🔥 同じような処理が複数ページに散らばりがち  → 複数ファイルに変更が及びがち 再利用できる形にしたい ♻ 目的:メンテしやすく、再利用しやすく

    高階関数(Higher-order function)にしてみる データフェッチ処理を再利用したい ” 少なくとも以下のうち1つを満たす関数である。 • 関数(手続き)を引数に取る • 関数を返す 高階関数 - Wikipedia 高階関数とは
  17. 17 const MyPage: NextPage<MyPageProps> = ({ name }) => {

    return <h1>Hello {name} !</h1> } MyPage.getInitialProps = async (context) => { const user = await getUser() if (!user) { return redirect('/login', context.res) } return { name: user.name } } export default MyPage 例:ユーザー認証が必要なマイページ 認証されていない場合は /login へ リダイレクトする データフェッチ処理を再利用したい getServerSideProps / getStaticProps でもできます
  18. 18 const MyPage: NextPage<MyPageProps> = ({ name }) => {

    return <h1>Hello {name} !</h1> } MyPage.getInitialProps = async (context) => { const user = await getUser() if (!user) { return redirect('/login', context.res) } return { name: user.name } } export default MyPage 例:ユーザー認証が必要なマイページ 認証されていない場合は /login へ リダイレクトする ここを切り出したい データフェッチ処理を再利用したい
  19. 19 export function withAuthenticated<Props>( getInitialProps?: GetInitialPropsWithUser<Props>, ): GetInitialProps<Props> { return

    (context) => { const user = getUser() if (!user) { return redirect('/login', context.res) } if (getInitialProps) return getInitialProps(context, user) } } ユーザー認証を行う高階関数を作ってみる type GetInitialProps<Props> = ( context: NextPageContext, ) => Props | Promise<Props> type GetInitialPropsWithUser<Props> = ( context: NextPageContext, user: User, ) => Props | Promise<Props> 型は自作
  20. 20 export function withAuthenticated<Props>( getInitialProps?: GetInitialPropsWithUser<Props>, ): GetInitialProps<Props> { return

    (context) => { const user = getUser() if (!user) { return redirect('/login', context.res) } if (getInitialProps) return getInitialProps(context, user) } } ユーザー認証を行う高階関数を作ってみる type GetInitialProps<Props> = ( context: NextPageContext, ) => Props | Promise<Props> type GetInitialPropsWithUser<Props> = ( context: NextPageContext, user: User, ) => Props | Promise<Props> 型は自作 getInitialProps を受け取り getInitialProps を返す (高階関数)
  21. 21 export function withAuthenticated<Props>( getInitialProps?: GetInitialPropsWithUser<Props>, ): GetInitialProps<Props> { return

    (context) => { const user = getUser() if (!user) { return redirect('/login', context.res) } if (getInitialProps) return getInitialProps(context, user) } } type GetInitialProps<Props> = ( context: NextPageContext, ) => Props | Promise<Props> type GetInitialPropsWithUser<Props> = ( context: NextPageContext, user: User, ) => Props | Promise<Props> 型は自作 ユーザー認証を行う高階関数を作ってみる
  22. 22 export function withAuthenticated<Props>( getInitialProps?: GetInitialPropsWithUser<Props>, ): GetInitialProps<Props> { return

    (context) => { const user = getUser() if (!user) { return redirect('/login', context.res) } if (getInitialProps) return getInitialProps(context, user) } } type GetInitialProps<Props> = ( context: NextPageContext, ) => Props | Promise<Props> type GetInitialPropsWithUser<Props> = ( context: NextPageContext, user: User, ) => Props | Promise<Props> 型は自作 引数で受け取った getInitialProps を最後に 実行する ユーザー認証を行う高階関数を作ってみる
  23. 23 export function withAuthenticated<Props>( getInitialProps?: GetInitialPropsWithUser<Props>, ): GetInitialProps<Props> { return

    (context) => { const user = getUser() if (!user) { return redirect('/login', context.res) } if (getInitialProps) return getInitialProps(context, user) } } type GetInitialProps<Props> = ( context: NextPageContext, ) => Props | Promise<Props> type GetInitialPropsWithUser<Props> = ( context: NextPageContext, user: User, ) => Props | Promise<Props> 型は自作 引数で受け取った getInitialProps を最後に 実行する ユーザー情報も渡す ユーザー認証を行う高階関数を作ってみる
  24. 24 const MyPage: NextPage<MyPageProps> = ({ name }) => {

    return <h1>Hello {name} !</h1> } MyPage.getInitialProps = withAuthenticated<MyPageProps>( async (_context, user) => { return { name: user.name } }, ) export default MyPage const MyPage: NextPage<MyPageProps> = ({ name }) => { return <h1>Hello {name} !</h1> } MyPage.getInitialProps = async (context) => { const user = await getUser() if (!user) { return redirect('/login, context.res) } return { name: user.name } } export default MyPage ユーザー認証を行う高階関数を作ってみる 比較
  25. 25 const MyPage: NextPage<MyPageProps> = ({ name }) => {

    return <h1>Hello {name} !</h1> } MyPage.getInitialProps = withAuthenticated<MyPageProps>( async (_context, user) => { return { name: user.name } }, ) export default MyPage const MyPage: NextPage<MyPageProps> = ({ name }) => { return <h1>Hello {name} !</h1> } MyPage.getInitialProps = async (context) => { const user = await getUser() if (!user) { return redirect('/login', context.res) } return { name: user.name } } export default MyPage ユーザー認証を行う高階関数を作ってみる 比較
  26. 26 const MyPage: NextPage<MyPageProps> = ({ name }) => {

    return <h1>Hello {name} !</h1> } MyPage.getInitialProps = withAuthenticated<MyPageProps>( async (_context, user) => { return { name: user.name } }, ) export default MyPage const MyPage: NextPage<MyPageProps> = ({ name }) => { return <h1>Hello {name} !</h1> } MyPage.getInitialProps = async (context) => { const user = await getUser() if (!user) { return redirect('/login', context.res) } return { name: user.name } } export default MyPage 認証処理をキレイに 切り出せた! ユーザー認証を行う高階関数を作ってみる 比較
  27. 27 getServerSideProps でも const MyPage: NextPage<PremiumPageProps> = ({ name })

    => { return <h1>Hello {name} !</h1> } export const getServerSideProps = withAuthenticated<MyPageProps>( async (_context, user) => { return { props: { name: user.name } } }, ) export default MyPage getStaticProps でも 🆗
  28. 28 getServerSideProps でも getStaticProps でも 🆗 const MyPage: NextPage<PremiumPageProps> =

    ({ name }) => { return <h1>Hello {name} !</h1> } export const getServerSideProps = withAuthenticated<MyPageProps>( async (_context, user) => { return { props: { name: user.name } } }, ) export default MyPage export function withAuthenticated<Props>( getServerSideProps: ( context: GetServerSidePropsContext, user: User, ) => Promise<GetServerSidePropsResult<Props>>, ): GetServerSideProps<Props> { return async (context) => { const user = await getUser() if (!user) { return { redirect: { destination: '/login', permanent: false } } } return await getServerSideProps(context, user) } }
  29. 29 • 重ね掛けすると実行時間が伸びる ◦ async 関数が順に実行されるため ◦ 関数内部で API リクエストをする場合は実行時間が伸びていく

    注意 Page.getInitialProps = withAuth(withPremium(withAdmin()))
  30. 30 • NewsPicks はフロントエンド基盤を Next.js へ移行中 🚀 • 現行環境と画面挙動を合わせたいのでデータフェッチ処理を活用している •

    Apollo Client のクエリキャッシュを使いたいので getInitialProps を採用している • データフェッチ処理を再利用したい場合は高階関数パターンが有効 まとめ
  31. 31 最後に AlphaDrive/NewsPicks ではエンジニアを募集中です! https://recruit.alphadrive.co.jp/ https://recruit.newspicks.com/ 今日の内容を詳しく知りたい方🖐カジュアルにお話ししましょう! https://meety.net/group_talks/oFZxAQWvdeLA