Next.jsプロジェクトの設計改善を進める上で考えていたこと

## 前置き

こんにちは！「NewsPicks」Web Experience Unit(以下Webチーム)の田端です。
この記事は[NewsPicks アドベントカレンダー 2023](https://qiita.com/advent-calendar/2023/newspicks)の9日目の記事になります。
昨日はasaさんの[【SwiftUI】文字を表示するベストプラクティス（Text・Label / TextEditor・TextField）](https://qiita.com/_asa08_/items/609d3bb38f339db9851f)でした！

## 背景

Web版「NewsPicks」では、リニューアルプロジェクトが2年ほど前から進んでおり、デザイン、基盤技術ともに大きく刷新が行われています。
フロントエンドの利用技術については「Spring + Thymeleaf + CoffeeScript + jQuery」から 「Nextjs + React + graphql + TypeScript」へと移行が行われています。
(詳しくは[12月7日の鶴房さんのアドベントカレンダー記事](https://qiita.com/shuCrane/items/d8dcb9aa0bab5b4f775e#%E3%82%A2%E3%83%BC%E3%82%AD%E3%83%86%E3%82%AF%E3%83%81%E3%83%A3%E3%81%AE%E5%A4%89%E6%9B%B4)で！)

リニューアルプロジェクトが始まって以来、5名前後のWebチームが主となって開発を進めてきましたが、ありがたいことにこの数ヶ月でWebチーム以外にもコミッターが大きく増え、プロジェクトの発足から2年以上を経たのも相まって設計の見直し、改善をしたいと感じる機会が増えてきました。

## 本題

Webチームの基本指針として、できるだけ技術の移り変わりについていきたい、必要に迫られる前に利用技術のバージョンアップに追従することで利用者、開発者とも体験を上げたいと考えています。

背景の流れもあり、Web版「NewsPicks」ではNext.jsの`getInitialProps`から`getServerSideProps`へ移行したい！という話をきっかけに絶賛設計変更&リファクタリング中です。

### そもそも`getInitialProps`から`getServerSideProps`に移行する必要があるのか？

Next.js公式は、サーバーサイドとクライアントサイド両方で実行される`getInitialProps`について、今後`getStaticProps`か`getServerSideProps`を代わりに利用することを推奨しています。ページ表示に使うデータリクエストの要不要を明確にして2つを使い分け、パフォーマンスの向上を狙っています。
詳細は以下の公式ドキュメントを読んでいただければと思います。

https://nextjs.org/docs/pages/api-reference/functions/get-initial-props

https://nextjs.org/blog/next-13-4

この半年でWeb版NewsPicksの開発者がかなり増えた中で、「`getServerSideProps`使っていきたいですね！」という声をいただけた後押しもあり、今回の流れとなりました。

その中で`_app.tsx`内の`getInitialProps`に各ページ共通の処理をいくつか任せていることを再認識し、こちらの解消に取り組むことになりました。具体的には以下の課題と、その解決案を検討しました。

### 各ページ共通で必要な認証関連のリダイレクト処理が置かれていた

とある新規ページ開発時にはじめて`getServerSideProps`を導入した際、移行に伴ってこれまで`_app.tsx`内の`getInitialProps`共通で行なっていた認証関連のリダイレクト処理を別箇所で行う必要があることに気づきました。

```typescript:_app.tsx
App.getInitialProps = async (context: NextAppContext): Promise<AppProps> => {
    appProps = await NextApp.getInitialProps(context)
    const cookies = parseCookies(context.ctx)
    token = cookies["cookieKey"]
    
    // Cookieとユーザー情報次第でリダイレクト
    if(token && appProps.pageProps.userData){
        context.ctx.res.writeHead(302, {
          Location: "redirectURL"
        })
    }
}
```

#### `middleware`で共通の認証処理を巻き取る

認証関連のリダイレクト処理を移動するにあたり、方針は3パターンありました。

①. 共通処理をutilとして作成し、各ページの`getServerSideProps`で呼び出す
②. `_app.tsx`の`App`内のuseEffectに共通処理を移す
③. `middleware`で共通の認証処理を巻き取る


今回は
- 認証処理でありmiddlewareへの配置に違和感ないこと
- 各ページ実装する際に上記の内容を意識しなくていいため新規開発者の考慮すべきことを減らせること
- 元々各pagePropsで共通処理のためのデータを渡す手間を感じていたこと
- できればリダイレクト前に画面のチラつきを出したくない

といった理由から③の「`middleware`で共通の認証処理を巻き取る」を選択しました。

https://nextjs.org/docs/pages/building-your-application/routing/middleware

```typescript:_middleware.ts
export const middleware: NextMiddleware = async (request: NextRequest) => {
  const cookie = request.headers.get('cookie') || ''
  const token = request.cookies['cookie_key']
  const response = await fetch('URL', {
    method: 'GET',
    headers: { cookie },
  })

  // Cookieとユーザー情報次第でリダイレクト
  if (token && response.json().userData === 'hoge') {
    return NextResponse.redirect('redirectURL')
  }

  return NextResponse.next()
}

```

### 各ページ共通で使用するヘッダーに必要な情報を取得するクエリが叩かれていた

```typescript:_app.tsx
  App.getInitialProps = async (context: NextAppContext): Promise<AppProps> => {
      const { data: headerData } =
        await apiClient.query<GetHeaderDataQuery>({
          query: GetHeaderDataDocument,
        })
      return {headerData}
  }
```

今回はヘッダーの情報でしたが、これ以外にも各ページで利用するデータを各ページで設定したGraphQLのQueryで取得しており、「新規ページを開発するにあたり各ページ共通で取得すべきデータを知っている必要がある」という状態は課題として持っていました。

```graphql:samplePage.graphqls
query samplePage {
  samplePageData {
    ...samplePageData
  }

  userData {
    ...UserData
  }
  headerData {
    ...HeaderData
  }
  # And More!
}
```

#### 各ページ共通で利用するデータをまとめたクエリを準備する

そこでヘッダーの情報取得を`_app.tsx`から剥がすのと同時に、各ページ共通で利用するデータを`CommonData`としてまとめ、新規ページにおいてはページ特有のQueryに併せてこれさえ呼べばいいという状態にしました。

```graphql:CommonData.graphqls
# 各ページ共通で必要になるデータ

query GetCommonData {
  userData {
    ...UserData
  }
  headerData {
    ...HeaderData
  }
  # And More!
}
```

あとは`graphql-codegen`で上記から生成されたQueryを各`page.tsx`で呼び出せば完了です。
```typescript:hoge.page.tsx
  const { data: commonData } = await apiClient.query<CommonDataQuery>({
    query: CommonDataDocument
  })
```

これまでは各ページで必要なデータを1つ1つ記載していたので、かなり楽になりました。

# 設計改善に対する考え

本記事では

- 技術の移り変わりにはEOLなど必要に迫られるなどの理由がなくとも追従していきたい
  - 追従に伴って設計の見直しを定期的に入れて開発者体験を上げたい
- `getInitialProps`から`getStaticProps`/`getServerSideProps`に移行する際は`_app.tsx`内の`getInitialProps`で共通化している処理を忘れないよう気をつけたい
- 共通化している処理は各ページごとに処理を書き下すのではなく、新規開発者が意識せずとも実現される状態にしたい

といった話をさせていただきました。
こうしたいと思いつつも後回しにしがちな面があるので、定期的にリファクタリングしていきたいですね。

ここまでご覧いただきありがとうございました。
明日は私と同じくWebチームの西さんが投稿してくれます。お楽しみに！

https://qiita.com/advent-calendar/2023/newspicks