Web開発ログ | エンジニアに役立つ情報 | 開発の中での気づきを発信

高卒Devlog

ChatGPTを使ったReactカルーセル開発【プロンプト駆動開発】

thumbnail

こんにちは! RYOTAです!

当記事をご覧くださりありがとうございます!

今話題のChatGPTを導入してReactのコンポーネント開発にトライしてみました。

はじめに

タイトル通りとなりますがChatGPTでReactのコンポーネントが開発出来るかトライしてみました。
今回は実験を兼ねて、なるべく手修正を施さないようにし、プロンプト駆動で全て改善出来るかのチャレンジをしました。
題材として当ブログのページトップに使えそうな記事スライダーが表示される、カルーセルコンポーネントの実装を試しています。

結論から言ってしまうと「出来ないことはなさそう!」「一から開発するより遥かに早い!」ということが分かりました。
実際のプロンプトと出力されたコードを載せていこうと思うので是非最後までご覧くださいませ。
GPT-4 (ChatGPT PLUS) で検証しています。

トライ1

Reactでカルーセルが表示されるコンポーネントを書いて

まず手始めに雑な指令をしてみようと思います。
実際にChatGPTに送ったプロンプトがこちらです。

// Reactでカルーセルが表示されるコンポーネントを書いて
// TailwindCSSベースで実装して
// TypeScriptベースで実装して

上記の出力がこちら

(コンポーネントのコード)

// CarouselComponent.tsx

import React from 'react'
import { Carousel } from 'react-responsive-carousel'
import 'react-responsive-carousel/lib/styles/carousel.min.css'
import './CarouselComponent.css'


type CarouselItem = {
  id: number
  image: string
  altText: string
}


interface CarouselComponentProps {
  items: CarouselItem[]
}


export const CarouselComponent: React.FC<CarouselComponentProps> = ({
  items,
}) => {
  return (
    <div className="carousel-container">
      <Carousel
        showThumbs={false}
        infiniteLoop
        autoPlay
        showStatus={false}
        showIndicators={false}
        interval={5000}
      >
        {items.map((item) => (
          <div key={item.id} className="carousel-item">
            <img
              src={item.image}
              alt={item.altText}
              className="w-full h-full object-cover"
            />
          </div>
        ))}
      </Carousel>
    </div>
  )
}

(呼び出し部分のコード)

// App.tsx
import React from "react";
import CarouselComponent from "./CarouselComponent";

const items = [
  {
    id: 1,
    imageUrl: "https://example.com/image1.jpg",
    title: "Image 1",
  },
  {
    id: 2,
    imageUrl: "https://example.com/image2.jpg",
    title: "Image 2",
  },
  {
    id: 3,
    imageUrl: "https://example.com/image3.jpg",
    title: "Image 3",
  },
];

const App: React.FC = () => {
  return (
    <div className="App">
      <header className="App-header">
        <h1 className="text-4xl font-bold mb-6">Carousel Demo</h1>
      </header>
      <main className="w-full h-screen p-4">
      <CarouselComponent items={items} />
      </main>
    </div>
  );
};
      
export default App;


....すごい...笑
ただ、react-responsive-carouselというライブラリを使おうとしていたので、こちらを使わないように再度指示してあげます。
(ライブラリまでよしなに選んでくれるの、これはこれで凄すぎだろ...)

react-responsive-carousel を使わずCSSは独自実装して

てな訳で修正のプロンプトを投げてあげます。

react-responsive-carousel を使わずCSSは独自実装して

(出力がこちら)

// Carousel.tsx

import React, { useState, useEffect, useRef } from 'react'

type CarouselItem = {
  id: number
  image: string
  altText: string
}

interface CarouselProps {
  items: CarouselItem[]
  interval?: number
}

export const Carousel: React.FC<CarouselProps> = ({
  items,
  interval = 5000,
}) => {
  const [activeIndex, setActiveIndex] = useState(0)
  const timeoutRef = useRef<NodeJS.Timeout>()

  function resetTimeout() {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current)
    }
  }

  useEffect(() => {
    resetTimeout()
    timeoutRef.current = setTimeout(
      () =>
        setActiveIndex((prevIndex) =>
          prevIndex === items.length - 1 ? 0 : prevIndex + 1
        ),
      interval
    )

    return () => {
      resetTimeout()
    }
  }, [activeIndex, interval, items.length])

  return (
    <div className="relative overflow-hidden">
      <div className="carousel">
        {items.map((item, index) => (
          <div
            key={item.id}
            className={`carousel-item absolute w-full h-full top-0 transition-opacity duration-500 ease-in-out ${
              index === activeIndex ? 'opacity-100' : 'opacity-0'
            }`}
          >
            <img
              src={item.image}
              alt={item.altText}
              className="w-full h-full object-cover"
            />
          </div>
        ))}
      </div>
    </div>
  )
}

ぉお!!ライブラリが外れてピュアなクラスに置き換わっている!!

しかし、、、一見動きそうな感じがしたのですが実際に呼び出してみると表示がされませんでした
DOM自体は作られていますが、余計なCSSが当たっているようですね。
表示されないとお話にならないので、とりあえず修正のプロンプトを投げてみます。

表示されないから修正して

表示されないから修正して

※画像は表示されましたが、カルーセルの挙動がなかったので一旦スキップ。

画像の表示サイズを横300px縦200pxで表示しつつ、横並びで画像が動くよう修正して

画像の表示サイズを横300px縦200pxで表示しつつ、横並びで画像が動くよう修正して

ここまで指定してあげるとようやく3枚の画像が一定間隔で切り替わる挙動になってくれました。
(1枚目)

(2枚目)

(3枚目)

画像だとわかりずらいですが、上記の3枚がスムーズに切り替わる挙動となっていました。
しかし目的のものとちょっと違ったので再度修正してみます。

画像サイズがバラバラでも同じサイズに自動調整されるよう修正して

画像サイズがバラバラでも同じサイズに自動調整されるよう修正して

※挙動の変化なし

横に3枚表示されて真ん中の1枚だけが少し大きく表示されるよう修正して

横に3枚表示されて真ん中の1枚だけが少し大きく表示されるよう修正して

※これ以降は出力されるコードにほぼ変化が起きなく、挙動の改善が見られませんでした

ここまでやってみて多少の改善はされましたが、根本的な挙動に変化が見られなかったのでプロンプトを大幅に変えてみます。

トライ2

挙動やルールを明示的に与えて、かつ項目毎にまとめるようプロンプトを大幅に変更してみました。

改善版: プロンプト

実際に投げたプロンプトはこちらです。

ブログの一番上に挿入するパーツを実装してください。

要件は以下の通りです。

# ブログトップ・カルーセル
1. ブログの一番上に挿入するカルーセルのコンポーネント実装

## 要件: 1
1. 見た目
  1-1. 画像が3個、横に並ぶように実装してください。
    1-1-1. 縦:横比は全て、1:1.78
	1-2. 真ん中が少し大きく、両サイドが少し小さく表示してください。
		1-2-1. 真ん中: 横300px
    1-2-2. 両サイド: 横200px
  1-3. それぞれの画像のmarginは16pxにしてください。
  1-4. border-radiusは5pxにしてください。
  1-5. サイズがバラバラな画像でも同じサイズに調整してください。
  1-6. コンポーネント全体の大きさは画面幅いっぱいに広がるようにしてください。
  1-7. 画像は全て画面内に表示してください。
  1.8. 画面の中央に配置してください。

2. 動き
  2-1. 画像を横にスライドして並び替えてください。
		2-1-1. 待機時間はPropsで指定
    2-1-2. 常に真ん中が大きくなるよう表示
	2-2. スライド時間は1秒間にしてください。
  2-3. 滑らかに横に画像をスライドしてください。

## 要件: 2
1. Reactで実装してください。
  1-1. Functional Componentベース
  1-2. export default を使用
2. TypeScriptベースで実装してください。
3. CSSはTailwind CSSで実装してください。
4. Propsは以下の通りにしてください。
  4-1. images: 画像の path と alt を指定
  4-2. interval: カルーセルの待機時間
5. ライブラリは使わず独自のCSSで実装してください。


改善版: アウトプット

// Carousel.tsx
import React, { useState, useEffect } from 'react'
import { useWindowSize } from './useWindowSize'

interface CarouselProps {
  images: { path: string; alt: string }[]
  interval: number
}

export default function Carousel({ images, interval }: CarouselProps) {
  const [currentIndex, setCurrentIndex] = useState(0)
  const windowSize = useWindowSize()

  useEffect(() => {
    const timer = setTimeout(() => {
      setCurrentIndex((currentIndex + 1) % images.length)
    }, interval)
    return () => clearTimeout(timer)
  }, [currentIndex, interval])

  return (
    <div
      className="w-full h-auto flex items-center justify-center overflow-hidden relative"
      style={{ height: 'calc(300px / 1.78)' }}
    >
      {images.map((image, index) => (
        <img
          key={index}
          src={image.path}
          alt={image.alt}
          className={`absolute object-cover ${
            index === currentIndex ? 'w-72 h-auto' : 'w-48 h-auto'
          } rounded-md mx-4 transition-all duration-1000 ease-in-out transform ${
            currentIndex === index ? 'scale-125' : ''
          }`}
          style={{
            left: `calc(50% - ${(index - currentIndex) * (200 + 16)}px)`,
            zIndex: index === currentIndex ? 10 : 1,
          }}
        />
      ))}
    </div>
  )
}


// useWindowSize.ts
import { useState, useEffect } from 'react'

interface WindowSize {
  width: number
  height: number
}

export function useWindowSize(): WindowSize {
  const [windowSize, setWindowSize] = useState<WindowSize>({
    width: window.innerWidth,
    height: window.innerHeight,
  })

  useEffect(() => {
    const handleResize = () => {
      setWindowSize({ width: window.innerWidth, height: window.innerHeight })
    }

    window.addEventListener('resize', handleResize)
    return () => window.removeEventListener('resize', handleResize)
  }, [])

  return windowSize
}

...!!???
カスタムhooks作って来たぞこの子!!!
実際に動かしてみたのがこちら
(1)

(2)

(3)

だいぶ良い感じ!
滑らかに動いて、ピックアップされる画像がぬるっと大きくなる!
期待した挙動にかなり近くなりました!

しかし、3枚の位置関係が画面中央に配置されず動的に左右に寄ってしまうため、このあたりは微修正が必要となりそうです。
この後もプロンプトを変更してみましたが、あまり改善がされなかったので、今後の宿題にしようと思います!

さいごに

今回はChatGPTを使ってプロンプト駆動開発にチャレンジしてみました。
結論は冒頭に述べた通り「出来ないことはなさそう」。
いくつか試して分かったのは、「プロンプトにはコツがあり、なるべく具体的に、かつ解釈の幅を持たせない」ことで期待通りのアウトプットに近づきそうだということです。

あくまでベースはChatGPTに、最終的なチェックと微修正を手作業にすれば爆速で開発が出来そうですね!
一発で期待通りの出力をさせるのが理想なので、引き続きプロンプトの研究は続けようと思います!

以上。最後までご覧いただきありがとうございました。