
DevelopersIO をリファクタリングしてみた
このブログは Next.js で動いています。
数年前には関わっていたこのプロジェクト。多忙で一時離れていましたが、今年の4月頃、現在の開発者からのヘルプを受けて、再びひそかに参加しています。4月以降にひっそりと実装された改修は、実は私が手掛けたものだったりします。
開発に携わる中で、コードベースが少し整理されていない状態だったため、リファクタリングをしながら機能改修を進めてきました。そして、最近リファクタリングが完了しました。
DevelopersIO のシステムは単純です。APIからレスポンスを受け取って、それを整形して表示するだけ。それなのに、野村萬斎さんを呼びたくなるようなコードになっていたのはなぜか……。
お弁当の底にあるくらいのスパゲッティの量でも、十分にこんがらがるということを身をもって知りました。そこで、どうすれば分かりやすいコードを書けるのか、振り返りながらまとめてみました。
コードはどれくらい変わったか?
今回のリファクタリングで、コードベースは大きく変わりました。Next.js 15へのアップデートや機能追加も行ったため単純なリファクタリングの結果ではありませんが、全体で7,000行ほど置き換えた計算になります。
git diff --shortstat b41ec79 73b0ff5
175 files changed, 10194 insertions(+), 6946 deletions(-)
綺麗なコードは本当に必要なのか?
「綺麗なコード」は開発者の理想であり、ビジネスには直接影響しないと思われがちです。
しかし、『A large scale empirical study of the impact of Spaghetti Code and Blob anti-patterns on program comprehension』という論文があります。この結論部分を引用します。
We report that, although single occurrences of Blob or Spaghetti code anti-patterns have little effect on code comprehension, two occurrences of either Blob or Spaghetti Code significantly increases the developers’ time spent in their tasks, reduce their percentage of correct answers, and increase their effort. Hence, we recommend that developers act on both anti-patterns, which should be refactored out of the source code whenever possible. We also recommend further studies on combinations of anti-patterns rather than on single anti-patterns one at a time.
要約すると、単一のスパゲッティコードやBlobアンチパターン(1つのクラスに責任が集中している状態)だけではコードの理解度に与える影響は小さいが、複数回出現すると、タスク完了にかかる時間と労力が増加し、理解度が下がるため、対処すべきである、という内容です。
つまり、開発に要する時間が伸びるため、結果的にビジネスにも影響を与えます。
実際に、今回のDevelopersIOの改修でも、少し機能を追加するだけでも無数のコードを読まなければならず、別のコンポーネントや型の整合性を意識したり、関数のインターフェースのために型が嘘をついていたりしたため、本当に修正が困難な状態でした。
しかし、リファクタリングを終えた今では、大規模な改修でも影響範囲を局所的に抑えられます。リファクタリングをしていなければ、あり得ないほどの分岐と複雑なロジックが必要になっていたでしょう。
どんなコードを書いたか?
小学生が夢見るような「すごいエンジニア」が書くコードではありません。ですが、**「10人が読んで、10人が理解し、10人が同じように書けるコード」**を目指しました。
ディレクトリ構成について
Bullet Proof React を参考に、ディレクトリ構成を組んでいます。
これは、年次ごとのイベント写真・動画管理に似ています。
たとえば、年を基準に管理すると以下のようになります。
├── 2024
│ ├── 体育祭
│ │ ├── 開催式.mp4
│ │ └── 閉会式.mp4
│ └── 文化祭
│ ├── 開会式.mp4
│ └── 閉会式.mp4
└── 2025
├── 体育祭
│ ├── 開催式.mp4
│ └── 閉会式.mp4
└── 文化祭
├── 開会式.mp4
└── 閉会式.mp4
一方、イベントを基準に管理する場合はこうなります。
├── 体育祭
│ ├── 2024
│ │ ├── 開催式.mp4
│ │ └── 閉会式.mp4
│ └── 2025
│ ├── 開会式.mp4
│ └── 閉会式.mp4
└── 文化祭
├── 2024
│ ├── 開会式.mp4
│ └── 閉会式.mp4
└── 2025
├── 開会式.mp4
└── 閉会式.mp4
今回のブログでは、features
という単位でグループ分けをしています。
個人的な好みではありますが、ディレクトリを何度もまたいでファイルを探すのが大変だと感じています。Article
という feature
の中に、型情報やコンポーネント、React Hooksなどをまとめて置いておけば、探すときは Article
以下だけを見ればいいので楽です。
また、features
でグループ分けをすると、それらが使える範囲が狭まります。例えば ArticleCard
は記事のカードにしか使えず、イベントの表示には使えない、と明確に捉えられます。これが components
以下で管理されていると、記事以外でも使える Card
という共通コンポーネントを作ろうとして、逆に管理が煩雑になることもあります。
コンポーネントを単純にする
コンポーネントは、極力単純に保つのが理想です。必要最小限の string
や number
型などのみを受け取り、それをそのまま表示するだけのコンポーネントに保ちます。日付関連だけは少し複雑なので、変換ロジックをコンポーネント内部に入れてもよいでしょう。
本当にシンプルな例ですが、以下のように表示するだけのコンポーネントを目指しました。トップページとかで表示されている記事一覧のカードみたいなものと思っていただけたら幸いです。
interface Props {
title: string
url: string
image: string
excerpt?: string
}
export const ArticleCard = ({title, url, excerpt}: Props) => {
return (
<a href={url}>
<image src={image} />
<p>{title}</p>
{excerpt && <span>{excerpt}</span>}
</div>
)
}
これを、Contentful
からのレスポンスをそのまま渡し、内部でサムネイルを探すロジックを入れると、他の部分でも同じような変換ロジックが必要になり、さまざまな場所でエンジニアの匙加減で変換処理が行われこんがらがります。
interface Props {
article: Article
assets: string
}
export const ArticleCard = ({article, assets}: Props) => {
const asset = assets.find((asset) => findAssetByArticle(article));
const image = asset.file.url
return (
<a href={url}>
<image src={image} />
<p>{article.fields.title}</p>
{article.fields.excerpt && <span>{article.fields.excerpt}</span>}
</div>
)
}
もちろん、エフェクトを扱ったり、input
で値を扱ったり、コンポーネント側でデータを取得したい場合もあります。しかし、そういった作業を関数に切り出すだけでも、コードは格段に楽になります。
データの取得時に整形を行う
データを取得して整形するというフローは必要です。このブログの例でいけば、Contentful のレスポンスからサムネイル画像を取り出したり、タグを文字列の配列型から必要なデータを含むオブジェクト型にする作業です。私はデータの整形をコンポーネントで行う代わりに、データの取得タイミングで整形するようにしました。
大切なのは、「どこでやるか」を揃えることです。今あげたような作業は複雑でそれ自体を避けることはできません。ですが、その複雑な作業を 1 つの場所に集約することはできます。綺麗なコードの節で引用したように、複雑さが複数箇所に跨ったときに理解が困難になるのですからそれをまとめればいいのです。今回の場合はデータの整形が複雑な処理なので、Page Components(fetch → parse)→ Some Components(props) という流れを徹底させて、fetch と parse をする関数で頑張るようにします。
まず、Zodでスキーマを定義し、コンポーネント側では、Article
型から必要な情報を Pick
できるようにします。
// features/articles/models/index.ts
import z from 'zod';
export const Article = z.object({
title: z.string(),
slug: z.string(),
firstPublishedAt: z.string().optional(),
thumbnail: Thumbnail,
author: z.object({
id: z.string(),
displayName: z.string(),
slug: z.string(),
thumbnail: Thumbnail,
}),
})
export type Article = z.infer<typeof Article>
次に、Contentful からデータを取得して Article
スキーマで parse
する関数を用意すれば完了です。今回は親になるページコンポーネントでデータを取得してそれを子に渡すだけなので、ページで fetch を呼び出していい感じのレイアウトにしていく感じです。
// features/articles/api/index.ts
export const fetchArticle = async (slug: string) => {
// データの取得
const response = await get('slug')
/**
* サムネイル画像を特定したり、タグを変換したりする
* */
const parsed = Article.safeParse({...response.items[0].fields, thumbnail, tags})
if (!parsed.success) {
console.error(z.prettifyError(parsed.error))
return null
}
return parsed.data
}
そのほか
Biomeで簡単な静的解析はしていますが、あまり厳密にはしていません。ガードレールをしっかり敷きたいのであればESLintでルールを書いてもいいのですが、フォーマットと静的解析が一瞬で終わる味を覚えてしまうと、どうしてもBiomeを利用したくなりますね。
そして。DevelopersIOの特性上、エラーハンドリングとフォームデータの扱いについては今回触れることができませんでした。仕事ではこのあたりも扱っているので、気になる方は聞いてください。
さいごに
結局のところ、今回行ったのは下記の2つです。
- 末端のコンポーネントを単純に保つ
- Page Components(fetch → parse)→ Some Components(props) という流れを徹底させる
どこで複雑さを捌いて、どこをシンプルに保つかを考慮しています。もし他のエンジニアがプロジェクトに参加したとしても、見た目を変えたいなら末端のコンポーネントを見ればよく、データの取得を見直したいならページ側を追えばいいので、作業の見通しがつきやすくなっているはずです。そして、プログラムの境界がタスクの境界にできるので、きっとAIでも意図したコードを出力しやすくなるでしょう。
ぜひ、皆さんもリファクタリングに挑戦してみませんか?