UPSIDERでエンジニアをしている太田です。 (@Hide55832241)
Claude CodeやCursor、GitHub CopilotなどAIによるコード生成が当たり前の時代になりました。
確かに開発速度は上がるかもしれません。
しかし、その代償として私たちは何を失っているのでしょうか?
※ 記事内のコード例は主にTypeScript、Reactを前提としたものになっています
失われる学習機会
つまずきから学ぶ機会が失われる
コードを書いていて想定外のことが発生したとき、そこに学習の機会があります。
頭の中ではこう考えたけど実際に書いてみたら想定外のことが発生し考え直す必要に迫られる。
こうした経験は、エンジニアとしての理解を深める貴重な瞬間です。
納得のできる解決法を見つけたときに、手応えを感じ知識が自分のものへ定着していきます。
しかし、考える前にAIがコードを出力したらその瞬間は訪れません。
考えてからAIにコードを生成させても、実装の過程で得られたはずの学習の機会は失われます。
繰り返しから学ぶ機会が失われる
繰り返しの作業ほど無駄なものはないと思うかもしれません。
しかしそこにも学習の機会があり、スキルを定着させる機会でもあります。
たとえばReactで汎用的なボタンコンポーネントや入力コンポーネントを作る。
車輪の再発明に思えるかもしれませんが、実際に作ってみると難しいものです。
それを自分で考え何度も作り直し、都度課題をみつけ改善した経験がある人と、誰かが作ったものを使っているだけの人では学習機会とスキルの定着に差がつくのではないでしょうか?
AIに生成させた場合も誰かが作ったものを使っているだけの人と似ているかもしれません。
自分で考え、試行錯誤していないのですから。
汎用コンポーネントの例
デザインだけでも考慮すべき点が多い
疑う機会が失われる
既存のコードをコピーして少し変更するだけで新機能を実装できることがあります。 一見効率的に見えますが、ここにも重要な学習機会が隠れています。
ReactでTanstackQueryを使ってAPIからデータ取得するコードを実装する例を見ていきましょう。
APIからカテゴリー一覧を取得しなんらかの処理をするとします。
useFetchArticles.ts
export const useFetchArticles = ({ config, params, }: UseFetchArticlesOptions) => { const queryConfig: QueryConfig<typeof fetchArticles> = { ...config, queryKey: ['articles'], queryFn: () => fetchArticles({ params }), } return useQuery(queryConfig) }
記事一覧を取得する useFetchArticles hookがすでに存在します。
これを参考に同じようにカテゴリー一覧を取得するhookを作成します。
useFetchCategories.ts
export const useFetchCategories = ({ config, params, }: UseFetchCategoriesOptions) => { const queryConfig: QueryConfig<typeof fetchCategories> = { ...config, queryKey: ['categories'], queryFn: () => fetchCategories({ params }), } return useQuery(queryConfig) }
コピーして命名、呼び出すAPIなどを変更すれば完成です。
なんの問題もなさそうに見えますが、既存のコードを疑うことでここにも学習の機会があります。
- 命令的に呼び出したくなったら?
- パラメータが確定するまでAPIを呼ばないようにするには?
- Suspenseを使いたくなったら?
既存のコードを参考にした場合対応することが難しいことに気づきます。
命令的に呼び出すには?
const queryConfig: QueryConfig<typeof fetchCategories> = { ...config, queryKey: ['categories'] queryFn: () => fetchCategories({ params }), } queryClient.fetchQuery(queryConfig)
fetchQuery を使用することで命令的に呼び出すことができます。
しかし useQuery をラップした useFetchCategories を転用できず、 useFetchCategories と同じ queryConfig を再度定義しないといけません。
queryConfig を参照できるようにすることでコードが共通化できます。
パラメータが確定するまでAPIを呼ばないようにするには?
enabled と skipToken を使用する方法を紹介します。
const [params, setParams] = useState<Params | null>(null) useFetchCategories({ config: { enabled: params !== null } params: params as any // nullは設定できない })
enabled を使用する方法ではパラメータが確定していない場合、パラメータに指定する型が不正となるため as any などで型に嘘をつき無理やり定義しないといけません。
const [params, setParams] = useState<Params | null>(null) useQuery( params === null ? { queryKey: [], queryFn: skipToken } : { ...config, queryKey: ['categories'] queryFn: () => fetchCategories({ params }), } )
skipToken を使う方法では型を無理やり変換することなく定義できます。
しかし useQuery をラップした useFetchCategories を転用できず、 useFetchCategories と同じ queryConfig を再度定義しないといけません。
queryConfig を参照できるようにすることでコードが共通化できます。
Suspenseを使いたくなったら?
const queryConfig: QueryConfig<typeof fetchCategories> = { ...config, queryKey: ['categories'] queryFn: () => fetchCategories({ params }), } useSuspenseQuery(queryConfig)
useSuspenseQuery を使用可能です。
しかし useQuery をラップした useFetchCategories を転用できず、 useFetchCategories と同じ queryConfig を再度定義しないといけません。
queryConfig を参照できるようにすることでコードが共通化できます。
このように既存のコードを疑うことで考え、学習する機会が生まれます。
今回の例では queryConfig を共通化することでほとんどのケースに対応できることがわかります。
また queryOptions という関数が存在することに気づくきっかけとなるかもしれません。
修正後コード例
export const categoriesQueryOptions = ({ config, params, }: QueryConfig<typeof fetchCategories>) => queryOptions({ ...config, queryKey: ['categories'] queryFn: () => fetchCategories({ params }), }) // useQueryでもcategoriesQueryOptionsを使える useQuery(categoriesQueryOptions({})) // useSuspenseQueryでもcategoriesQueryOptionsを使える useSuspenseQuery(categoriesQueryOptions({})) // fetchQueryでもcategoriesQueryOptionsを使える queryClient.fetchQuery(categoriesQueryOptions({})) // skipTokenを使用する場合でもcategoriesQueryOptionsを使える const [params, setParams] = useState<Params | null>(null) useQuery( params === null ? { queryKey: [], queryFn: skipToken } : categoriesQueryOptions({}) )
きれいに解決できたように見えますが、useQueryに独自の処理を追加したい場合にやはりuseQueryをラップしたカスタムhookを作る必要があることに気づくかもしれません。
しかしこのような複雑な状況が深い学習に繋がっていきます。
既存のコードを参考にAIに生成させるとこのような学習機会は失われます。
「確認するから大丈夫」という幻想
AIを使っても最終的には人が確認するから、学習機会が奪われることはないと思うかもしれません。
しかし「確認」と「実装」では、脳への負荷と学習効果が大きく異なります。
思考の過程をショートカットし、完成されたコードを読んで理解することと、ゼロから設計・実装することでは、得られる学びの深さに差があるのではないでしょうか。
コード量が増えれば学習量も増えるのでは?
AIによってむしろ生成するコードの量は増えるから学習量も増えるという意見もあるかもしれません。
もちろん学べることもあるでしょう。
しかし、本当に身につくのでしょうか?
学習には量も大事ですが質が伴っていないと学習の効果は下がります。
AIが生成したコードを少し修正する経験と、完全に自分で考え実装する経験では、前者の方が量は多くても後者の方が質が高く深い学びを得られるかもしれません。
受動的な学習と能動的な学習の違いとも言えます。
生成されたコードを読むだけでは、自分で書く場合に比べて学習効果は限定的でないでしょうか?
試行錯誤を重ね、「こんな解決方法があったのか」と自分で気付いたとき、手応えを感じ知識が自分のものへ定着していきます。
この感覚こそが、エンジニアとしての成長に欠かせない要素ではないかと考えます。
抽象的な理解と具体的な実装力のギャップ
AIを頻繁に使用することで生じるもう1つの問題は、「概念は理解しているが実装できない」というギャップです。
「この設計パターンを適用すればいい」と頭では分かっていても、実際にコードに落とし込む段階で手が止まる。
これは、抽象的な知識と具体的な実装力の間に差があるからです。
気づいたらAIなしでは実装できないエンジニアになっていた、ということもあるかもしれません。
AIの出力を適切に評価・修正できない状況に直面したとき、困ることになります。
同じようなコードを何度も書き、パターンを身体で覚え、やがて考えなくても書けるレベルに到達する。
この反復練習の機会が奪われ、知識と技能の乖離が広がっていきます。
学習機会とキャリア
AIは使う人のブースターのようなものかもしれません。
すでに基礎のある経験豊富なエンジニアであれば、生成されたコードの品質を適切に評価し、潜在的な問題を見抜き、より良い実装に改善できるかもしれません。
彼らにとってAIは、定型的なコードの記述を省略し、より創造的な部分に集中するためのツールとなりえます。
問題はまだキャリアの初期段階でAIに頼りすぎることです。
3年後、5年後にどのようなスキルを身につけていたいでしょうか?
AIに頼ることで短期的な生産性は上がるかもしれませんが、それが長期的なスキル獲得にどう影響するか、一度立ち止まって考えてみる価値があるかもしれません。
技術を深く追求する道もあれば、ビジネスとのバランスを取る道もあるでしょう。
ただ、どの道を選ぶにせよ、基礎的な実装力は多くの選択肢を可能にする土台となります。
まとめ
AIを使うなと言いたいわけではありません。
完全に避けるべきだとは思いませんし、私自身毎日AIを使っています。
AIに頼りすぎると前述のとおり学習機会は減るかもしれません。
しかし自分で考え実装した上でAIから別の視点を得ることができれば、学習機会はむしろ増える可能性もあります。
たとえば、自分が実装したコードをAIにレビューしてもらう。
パフォーマンスやセキュリティ、エッジケースなど、自分の専門外の視点から改善点を発見できるかもしれません。
AIはあらゆる分野でそれなりの知識を持っており、こうした使い方であれば強力な学習ツールになりえます。
私は日々以下のようなことを意識しながら、AIとの付き合い方を模索しています。
- 自分の専門分野とそうではない分野
- 必要な成果物の精度とAIによる成果物の精度のギャップ
- 生成された成果物の編集の可否や容易性
コード生成に限らず、自分の専門分野でAIを使うことによるトレードオフについて一度立ち止まって考えてみる価値があるのではないでしょうか。
We Are Hiring !!
株式会社UPSIDERでは現在積極採用をしています。 ぜひお気軽にご応募ください。
UPSIDER Engineering Deckはこちら📣