前置き
こんにちは!「NewsPicks」Web Experience Unit(以下Webチーム)の田端です。
この記事はNewsPicks アドベントカレンダー 2023の9日目の記事になります。
昨日はasaさんの【SwiftUI】文字を表示するベストプラクティス(Text・Label / TextEditor・TextField)でした!
背景
Web版「NewsPicks」では、リニューアルプロジェクトが2年ほど前から進んでおり、デザイン、基盤技術ともに大きく刷新が行われています。
フロントエンドの利用技術については「Spring + Thymeleaf + CoffeeScript + jQuery」から 「Nextjs + React + graphql + TypeScript」へと移行が行われています。
(詳しくは12月7日の鶴房さんのアドベントカレンダー記事で!)
リニューアルプロジェクトが始まって以来、5名前後のWebチームが主となって開発を進めてきましたが、ありがたいことにこの数ヶ月でWebチーム以外にもコミッターが大きく増え、プロジェクトの発足から2年以上を経たのも相まって設計の見直し、改善をしたいと感じる機会が増えてきました。
本題
Webチームの基本指針として、できるだけ技術の移り変わりについていきたい、必要に迫られる前に利用技術のバージョンアップに追従することで利用者、開発者とも体験を上げたいと考えています。
背景の流れもあり、Web版「NewsPicks」ではNext.jsのgetInitialProps
からgetServerSideProps
へ移行したい!という話をきっかけに絶賛設計変更&リファクタリング中です。
そもそもgetInitialProps
からgetServerSideProps
に移行する必要があるのか?
Next.js公式は、サーバーサイドとクライアントサイド両方で実行されるgetInitialProps
について、今後getStaticProps
かgetServerSideProps
を代わりに利用することを推奨しています。ページ表示に使うデータリクエストの要不要を明確にして2つを使い分け、パフォーマンスの向上を狙っています。
詳細は以下の公式ドキュメントを読んでいただければと思います。
この半年でWeb版NewsPicksの開発者がかなり増えた中で、「getServerSideProps
使っていきたいですね!」という声をいただけた後押しもあり、今回の流れとなりました。
その中で_app.tsx
内のgetInitialProps
に各ページ共通の処理をいくつか任せていることを再認識し、こちらの解消に取り組むことになりました。具体的には以下の課題と、その解決案を検討しました。
各ページ共通で必要な認証関連のリダイレクト処理が置かれていた
とある新規ページ開発時にはじめてgetServerSideProps
を導入した際、移行に伴ってこれまで_app.tsx
内のgetInitialProps
共通で行なっていた認証関連のリダイレクト処理を別箇所で行う必要があることに気づきました。
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
で共通の認証処理を巻き取る」を選択しました。
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()
}
各ページ共通で使用するヘッダーに必要な情報を取得するクエリが叩かれていた
App.getInitialProps = async (context: NextAppContext): Promise<AppProps> => {
const { data: headerData } =
await apiClient.query<GetHeaderDataQuery>({
query: GetHeaderDataDocument,
})
return {headerData}
}
今回はヘッダーの情報でしたが、これ以外にも各ページで利用するデータを各ページで設定したGraphQLのQueryで取得しており、「新規ページを開発するにあたり各ページ共通で取得すべきデータを知っている必要がある」という状態は課題として持っていました。
query samplePage {
samplePageData {
...samplePageData
}
userData {
...UserData
}
headerData {
...HeaderData
}
# And More!
}
各ページ共通で利用するデータをまとめたクエリを準備する
そこでヘッダーの情報取得を_app.tsx
から剥がすのと同時に、各ページ共通で利用するデータをCommonData
としてまとめ、新規ページにおいてはページ特有のQueryに併せてこれさえ呼べばいいという状態にしました。
# 各ページ共通で必要になるデータ
query GetCommonData {
userData {
...UserData
}
headerData {
...HeaderData
}
# And More!
}
あとはgraphql-codegen
で上記から生成されたQueryを各page.tsx
で呼び出せば完了です。
const { data: commonData } = await apiClient.query<CommonDataQuery>({
query: CommonDataDocument
})
これまでは各ページで必要なデータを1つ1つ記載していたので、かなり楽になりました。
設計改善に対する考え
本記事では
- 技術の移り変わりにはEOLなど必要に迫られるなどの理由がなくとも追従していきたい
- 追従に伴って設計の見直しを定期的に入れて開発者体験を上げたい
-
getInitialProps
からgetStaticProps
/getServerSideProps
に移行する際は_app.tsx
内のgetInitialProps
で共通化している処理を忘れないよう気をつけたい - 共通化している処理は各ページごとに処理を書き下すのではなく、新規開発者が意識せずとも実現される状態にしたい
といった話をさせていただきました。
こうしたいと思いつつも後回しにしがちな面があるので、定期的にリファクタリングしていきたいですね。
ここまでご覧いただきありがとうございました。
明日は私と同じくWebチームの西さんが投稿してくれます。お楽しみに!