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

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

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

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 / 芥川 周平

    View full-size slide

  2. 2
    自己紹介
    芥川 周平
    2017.04 -
    受託開発の会社でWeb系の新規開発を4つほど経験
    2021.09 -
    株式会社アルファドライブ / 株式会社ニューズピックス に所属
    Web Frontend Unit で NewsPicks のリアーキテクチャを行っている
    兵庫県姫路市🏯からフルリモート
    @aku11i

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  6. 6
    NewsPicks のリアーキテクチャについて
    NewsPicks ではフロントエンドのリアーキテクチャを行っている
    ● モノリスな Web 基盤からフロントエンド部分を剥がし、
    Next.js に載せ替えている
    ● 初期段階ではページの見た目や機能はそのままに基盤だけを載せ換える方針
    ● 新基盤でも画面挙動を合わせたいので
    レンダリング前に必要なデータを全て取得しておく必要がある
    → getInitialProps or getServerSideProps を活用する

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  9. 9
    ● 初めは getServerSideProps を使用していた
    ● どのページでも必要になる共通データの BFF へのリクエストを抑えたい話が出た
    ○ ヘッダーに表示するユーザー情報など
    ● Apollo Client で GraphQL のクエリ結果をキャッシュするのがシンプルっぽい 👀
    ○ getServerSideProps では実現できなかった(詳細は次ページで解説)
    → getInitialProps を採用することに
    なぜ getInitialProps ?

    View full-size slide

  10. 10
    データフェッチと Apollo Client のキャッシュ について
    BFF
    BFF
    getInitialProps (初回)
    getServerSideProps
    getInitialProps (2回目以降)

    View full-size slide

  11. 11
    BFF
    BFF
    getInitialProps (初回)
    getServerSideProps
    getInitialProps (2回目以降)
    様々なユーザーのクエリを実行する
    のでキャッシュを利用できない
    データフェッチと Apollo Client のキャッシュ について

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  14. 14
    データフェッチ処理を再利用したい話

    View full-size slide

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

    View full-size slide

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

    少なくとも以下のうち1つを満たす関数である。
    ● 関数(手続き)を引数に取る
    ● 関数を返す
    高階関数 - Wikipedia
    高階関数とは

    View full-size slide

  17. 17
    const MyPage: NextPage = ({ name }) => {
    return Hello {name} !
    }
    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 でもできます

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  28. 28
    getServerSideProps でも
    getStaticProps でも 🆗
    const MyPage: NextPage = ({ name }) => {
    return Hello {name} !
    }
    export const getServerSideProps = withAuthenticated(
    async (_context, user) => {
    return { props: { name: user.name } }
    },
    )
    export default MyPage
    export function withAuthenticated(
    getServerSideProps: (
    context: GetServerSidePropsContext,
    user: User,
    ) => Promise>,
    ): GetServerSideProps {
    return async (context) => {
    const user = await getUser()
    if (!user) {
    return { redirect: { destination: '/login', permanent: false } }
    }
    return await getServerSideProps(context, user)
    }
    }

    View full-size slide

  29. 29
    ● 重ね掛けすると実行時間が伸びる
    ○ async 関数が順に実行されるため
    ○ 関数内部で API リクエストをする場合は実行時間が伸びていく
    注意
    Page.getInitialProps = withAuth(withPremium(withAdmin()))

    View full-size slide

  30. 30
    ● NewsPicks はフロントエンド基盤を Next.js へ移行中 🚀
    ● 現行環境と画面挙動を合わせたいのでデータフェッチ処理を活用している
    ● Apollo Client のクエリキャッシュを使いたいので getInitialProps を採用している
    ● データフェッチ処理を再利用したい場合は高階関数パターンが有効
    まとめ

    View full-size slide

  31. 31
    最後に
    AlphaDrive/NewsPicks ではエンジニアを募集中です!
    https://recruit.alphadrive.co.jp/
    https://recruit.newspicks.com/
    今日の内容を詳しく知りたい方🖐カジュアルにお話ししましょう!
    https://meety.net/group_talks/oFZxAQWvdeLA

    View full-size slide