はじめに
フェンリル株式会社のインターンシップ 2026 で、フロントエンドコースに参加しました。
フェンリル株式会社:https://fenrir.co.jp
絶賛就活中(?)の高校1年生こと、tanahiro2010です。
大学には進学するつもりなのですが、就活向けのイベントやインターンが普通に楽しく、よく参加しています。
普段の個人開発でも React などを使ってフロントエンドを書くことはありますが、どちらかというと私はバックエンドの方が好きです。
フロントエンドが苦手というわけではないものの、得意と言い切れるほどでもありません。
特にデザイン面は AI にかなり任せてしまうことも多く、バックエンドのように自分の手でゴリゴリ書けている感覚はあまりありませんでした。
それでもフロントエンドコースを選んだのは、フェンリルさんのサイトで、普段とは異なる技術領域に挑戦しているインターン参加者の記事を見たことがきっかけです。
「せっかくインターンに参加するなら、自分が普段得意としている領域ではなく、あえて少し苦手意識のある分野に挑戦してみよう」
と思い、フロントエンドコースを選びました。
5日間という短い期間でしたが、実装そのものだけでなく、レビューを通して「管理画面をどう作るか」「どこに責務を置くか」をかなり考える機会になりました。
作ったもの
今回のインターンでは、コンテンツやユーザーなどを管理するための Web 管理画面のフロントエンドを開発しました。
主に、以下のような画面の実装に取り組みました。
- データの一覧画面
- データの詳細画面
- ユーザーの一覧画面
- ユーザーの詳細画面
一覧画面では、検索・ソート・ページネーションなどを実装し、多くのデータを扱いやすくしました。
また、複数の表示方法を切り替えられるようにするなど、管理画面として情報を確認しやすくするための UI についても考えました。
詳細画面では、対象となるデータの情報を確認したり、管理上必要となる操作を行ったりできるようにしました。
※インターンシップの具体的な課題内容については、一部表現を一般化しています。
技術構成
主に使った技術は以下です。
- React
- TypeScript
- TanStack Router
- Tailwind CSS
- axios
CSS や UI コンポーネントの設計については、仕様を満たしていれば比較的自由に選択できたため、私は Tailwind CSS を使って実装しました。
Tailwind CSS は className が長くなりやすいという面もありますが、短期間で画面を組むうえではかなり速く、今回のような開発では相性がよかったです。
画面より先に、扱いやすい設計を作る
今回の実装で特に意識したのは、画面側に処理を書きすぎないことです。
管理画面では、似た構造の一覧画面や詳細画面が複数登場します。
そのため、API 呼び出しやソート処理、共通する UI を画面ごとにすべて書いてしまうと、コンポーネントが大きくなり、見通しも悪くなります。
そこで、以下のように責務を分けることを意識しました。
- API 呼び出しは専用のクライアントに寄せる
- ページ表示に必要なデータ取得は Router の loader に寄せる
- ソートなどのロジックは hooks に分ける
- ページネーションや検索 UI は共通コンポーネントにする
API 呼び出しをクライアントに寄せる
API を呼び出す処理は、画面から直接 axios を呼ぶのではなく、専用の API クライアントを経由するようにしました。
例えば、イメージとしては以下のような形です。
await api.auth.login({ email, password });
await api.items.getItems();
await api.users.getUser(userId);
API の種類ごとに処理を分割し、それらをまとめたクライアントを用意しました。
export class ApiClient {
readonly auth = new AuthApi();
readonly users = new UsersApi();
readonly items = new ItemsApi();
}
export const api = new ApiClient();
こうしておくことで、画面側では、
「どの URL にリクエストするのか」
「axios にどのような設定が必要なのか」
といった細かい部分をあまり意識せず、
api.items.getItems();
のように、目的をベースに API を呼び出せます。
また、認証情報の付与や API から返ってきたエラーの変換など、複数の API で共通する処理についても、できるだけクライアント側にまとめました。
これによって、各画面や API メソッドで同じ処理を何度も書かずに済むようになります。
TanStack Router の loader を使う
一覧画面などでは、ページ表示時に必要となるデータを TanStack Router の loader で取得するようにしました。
実際のコードとは異なりますが、イメージとしては次のような形です。
export const Route = createFileRoute("/items/")({
component: () => <ItemList />,
loader: async () => {
const response = await api.items.getItems();
return response.items;
},
pendingComponent: () => <Loading />,
});
最初は useEffect でデータを取得することも考えていました。
しかしレビューを通して、URL に対応して表示するデータなど、ページそのものに必要な初期データについては Router の loader に寄せる方法もあると学びました。
useEffect は便利ですが、何でも入れてしまうと、
「いつこの処理が動くのか」
「なぜこのタイミングで通信しているのか」
が分かりづらくなる場合があります。
今回の開発では、
- ページ表示に必要な初期データ → loader
- 画面内で変化する表示状態 → React の state
というように、役割を分けて考える意識ができました。
ソート処理を hooks に分ける
一覧画面では、複数の条件でデータを並び替えられるようにしました。
例えば、
- ID
- 名前
- 数値
- 日時
- 状態
といった項目を使った並び替えです。
これらを画面コンポーネントの中にすべて書いてしまうと、JSX とロジックが混ざって読みづらくなります。
そこで、ソート処理を Custom Hook として分離しました。
const { sortedItems } = useItemSort(items, sort);
内部では、例えば次のような形でデータを並び替えます。
const sortedItems = useMemo(() => {
return [...items].sort((a, b) => {
switch (sortOption) {
case "value-ascending":
return a.value - b.value;
case "value-descending":
return b.value - a.value;
case "date-ascending":
return (
new Date(a.createdAt).getTime() -
new Date(b.createdAt).getTime()
);
case "date-descending":
return (
new Date(b.createdAt).getTime() -
new Date(a.createdAt).getTime()
);
default:
return 0;
}
});
}, [items, sortOption]);
このように分けることで、画面側は、
「ユーザーがどのソート方法を選んでいるか」
と、
「ソートされた結果をどのように表示するか」
に集中できます。
共通コンポーネント化したもの
管理画面では、似た UI が何度も登場します。
今回、特に共通化を意識したのは以下です。
PaginationSearchInputSortDropdownButton
扱うデータが違っていても、検索・ソート・ページネーションなど、一覧画面としての構造はかなり似ています。
そのため、画面ごとに UI を一から作り直すのではなく、共通コンポーネントとして切り出し、使い回せるようにしました。
短期間の開発では、とにかく目の前の画面を完成させたくなります。
ただ、同じような画面が複数ある場合、最初から少しだけ共通化を意識しておくことで、後から仕様を変更するときにも直しやすくなると感じました。
よくできたこと
今回よくできたと思うのは、処理を役割ごとに分け、画面側を比較的薄くできたことです。
特に以下は、自分の中でも成長を感じた部分です。
- API 呼び出しを専用の層に分けたこと
- ソート処理を hooks に分けたこと
- 共通 UI をコンポーネント化したこと
- TanStack Router の loader の使い方を理解できたこと
最初は「まず動けばいい」という気持ちで進めそうになりました。
しかしレビューをもらいながら、
「後から読む人にとって分かりやすいか」
「一つの画面に責務が集まりすぎていないか」
という点も考えるようになりました。
あと一歩だったこと
一番大きな反省点は Git の操作です。
作業ブランチの切り方をあまり整理せずに進めてしまったことで、途中から変更内容の整理に苦戦しました。
その結果、コンフリクトの解消などにかなり時間を使ってしまいました。
また、一つの変更に複数の機能が混ざってしまい、PR の単位について考え直す場面もありました。
これは、自分のブランチ運用や作業単位の決め方が十分に整理できていなかったことが原因だと思います。
コードを書く前に、
- どのブランチを起点にするのか
- 今回の PR では何を変更するのか
- どの程度の単位で commit するのか
を確認しておくべきでした。
また、変更を小さく commit し、こまめに他の変更を取り込んでいれば、もう少しスムーズに開発できたと思います。
もう一つの反省点は、事前調査です。
API の型やクライアントについて自分で実装を進めた後、OpenAPI などからコードを生成できるツールが存在することを知りました。
もちろん、自分で実装したことで API クライアントの構造について理解できた部分もあります。
一方で、短期間の開発では、
「そもそも既に使えるツールやライブラリはないか」
を最初に調べることも重要だと感じました。
「調べればよかった……」という感じです。
レビューで学んだこと
今回のインターンでは、PR レビューを通してかなり多くのことを学びました。
特に印象に残ったのは、単にコードが動くかどうかだけではなく、
- 責務の置き方
- エラー時の挙動
- アクセシビリティ
- セキュリティ
- 可読性
- ライブラリの適切な使い方
といった部分まで考える必要があることです。
ここからは、レビューを通して特に学びになった内容を一般化して振り返ります。
useEffect だけに頼らずデータ取得の場所を考える
ページ表示時のデータ取得について、最初は React コンポーネント内から取得する方法を考えていました。
しかし、ページに必要となるデータについては Router の loader に置くことで、画面コンポーネントの責務を減らせる場合があります。
単に「データが取得できるか」だけでなく、
「このデータはどの層で取得するのが自然なのか」
を考えることが重要だと学びました。
エラーをすべて同じものとして扱わない
認証が必要な画面では、API 通信が失敗した場合の処理も必要になります。
ここで、
「API が失敗した = 認証情報が無効」
と判断してしまうと、ネットワーク障害やサーバー側の一時的な問題でも、ユーザーに再ログインを要求してしまう可能性があります。
そのため、
- 認証に関するエラー
- サーバー側のエラー
- ネットワークエラー
などを区別し、それぞれに合った処理を考える必要があると学びました。
認証情報は保存場所だけ考えればよいわけではない
Web アプリで認証情報を扱う場合、
「どこに保存するか」
だけではなく、アプリケーション全体のセキュリティを考える必要があります。
例えば、JavaScript からアクセスできる場所に重要な情報を保存するのであれば、XSS 対策なども含めて考える必要があります。
一つの実装方法だけを見るのではなく、周辺のリスクも含めて設計する必要があると学びました。
label と input を正しく紐づける
ログインフォームなどの入力 UI についても、アクセシビリティの観点から改善できる部分がありました。
例えば、
<label for="email">メールアドレス</label>
<input id="email" name="email" type="email" />
のように、label と input を正しく関連付けます。
見た目だけでなく、
- ラベルをクリックしたときの操作
- ブラウザのオートコンプリート
- スクリーンリーダーなどの支援技術
まで含めて UI を考える必要があると学びました。
環境ごとに変わる値をコードに直接書かない
API の接続先など、環境によって変わる値については、コードへ直接書くのではなく環境変数などを利用する方が管理しやすくなります。
また、環境変数ファイルについても、
- リポジトリに含めてよい設定
- ローカル環境だけに置く設定
- 公開してはいけない値
を区別して扱う必要があります。
私はそれまで「.env は全部 Git に含めないもの」というイメージを持っていたので、利用するツールごとの仕組みや慣習について知るきっかけにもなりました。
useMemo は必要な場所で使う
React の useMemo についても学びがありました。
計算結果をキャッシュできる便利な API ですが、軽い処理に何でも useMemo を付ければ速くなるわけではありません。
むしろ、処理によってはコードが複雑になるだけの場合もあります。
React の API を知っているだけでなく、
「この処理に本当に必要なのか」
まで考えて使うことが大切だと感じました。
API クライアントにも設計の余地がある
API クライアントについても、最初に作った構造からさらに改善できる部分がありました。
例えば、
- 共通するエラー処理をまとめる
- 型を責務ごとに整理する
- class を使う必要が本当にあるか考える
- 同じような処理を共通化する
といった点です。
単に「API が呼べるクライアント」を作るだけでなく、機能が増えていったときにも保守しやすい構造を考える必要があります。
細かい命名や整形も読みやすさにつながる
変数名や値の名前についても、意味が直感的に分かる名前を使うことでコードを読みやすくできます。
例えば、昇順・降順を表すのであれば、
ascending
descending
のような名前を使うと意図が明確になります。
また、クラス名の組み立てを補助するライブラリや、Prettier・Biome などのフォーマッターを利用することで、コード全体の読みやすさを揃えることもできます。
一つ一つは小さな違いですが、複数人でコードを読む場合には重要な部分だと感じました。
AI との向き合い方
今回の開発を振り返ると、最近の私はフロントエンド開発で AI に頼りすぎていたと感じました。
AI を使うこと自体は悪いことではないと思っています。
ただ、自分で設計や実装を説明できないまま進めてしまうと、レビューで指摘をもらったときに理解が追いつかなくなります。
これからは AI を使いつつも、
- 自分で読める
- 自分で書ける
- 自分で説明できる
状態を目指したいです。
特にフロントエンドでは、
- UI の構造
- 状態管理
- データ取得
- 副作用の置き場所
- コンポーネントの責務
などについて、自分で判断できるように練習していきたいと思います。
まとめ
5日間のインターンシップを通して、管理画面のフロントエンドを開発しながら、React の書き方だけでなく、Web フロントエンドを設計するときの考え方まで学ぶことができました。
短期間の開発だったからこそ、できたことと反省点もかなりはっきりしました。
よくできたと思うのは、処理を役割ごとに分け、画面側を薄くし、共通化を意識できたことです。
一方で、
- Git のブランチ運用
- 作業単位の分け方
- 開発前の事前調査
については、まだ改善できる部分が多くありました。
次に開発するときは、コードを書く前に作業の進め方や利用できるツールを確認し、AI に頼るだけでなく、自分自身でも設計・実装・説明ができるようにしていきたいと思います。
そして何より、5日間とても楽しかったです!
レビューや相談に付き合ってくださったメンターの方々、インターンシップを運営してくださった皆さま、本当にありがとうございました!