풀스택 카운터 앱 - 실시간 클릭 집계 - 바이브코딩 레시피
입문 PHP + JavaScript Laravel + React AI: Claude

풀스택 카운터 앱 - 실시간 클릭 집계

리포지토리명 fullstack-click-counter
조회수 122 0건 제출

문제 설명

프로젝트 개요

Laravel 13.x 백엔드 API와 React 19 프론트엔드를 분리하여 구성하는 첫 풀스택 프로젝트입니다. 버튼을 클릭하면 카운트가 증가하고, 백엔드 DB에 저장된 총 클릭 수를 실시간으로 불러와 표시합니다.

필수 기능

  • Laravel API 서버: SQLite DB에 클릭 수 저장, RESTful API 엔드포인트 제공
    • GET /api/counter: 현재 카운트 조회
    • POST /api/counter/increment: 카운트 증가 (+1)
    • POST /api/counter/reset: 카운트 초기화 (0으로)
  • React 프론트엔드: Vite 기반 SPA, Tailwind CSS 스타일링
    • 큰 숫자로 현재 카운트 표시
    • "클릭!" 버튼 (클릭 시 API 호출하여 카운트 증가)
    • "초기화" 버튼 (카운트를 0으로 리셋)
    • 페이지 로드 시 자동으로 현재 카운트 불러오기
  • 에러 핸들링: API 호출 실패 시 사용자 친화적 메시지 표시
  • 로딩 상태: API 요청 중 버튼 비활성화 및 로딩 표시

선택 추가 기능 (보너스)

  • 클릭 애니메이션 효과 (숫자가 증가할 때 펄스 효과)
  • 클릭 히스토리 (최근 10개 클릭 타임스탬프 표시)
  • 다크 모드 토글
  • 총 클릭 수에 따라 배지/메시지 표시 (예: 100회 달성 시 "초보 클리커", 1000회 시 "클릭 마스터")

학습 목표

  • Laravel API 서버와 React SPA를 분리하여 구성하는 방법 이해
  • RESTful API 설계 및 CORS 설정
  • React에서 fetch/axios로 API 호출 및 상태 관리 (useState, useEffect)
  • Laravel 마이그레이션, 모델, 컨트롤러 기본 사용법
  • 프론트엔드와 백엔드 간 데이터 흐름 파악

제약 조건

  • • Laravel 13.x 사용 (최신 버전)
  • • React 19 사용 (최신 버전)
  • • Laravel은 API 서버로만 동작 (Blade 템플릿 사용 안 함)
  • • React는 Vite로 별도 프로젝트 생성
  • • DB는 SQLite 사용 (간단한 설정)
  • • Tailwind CSS로 스타일링
  • • CORS 설정으로 React에서 Laravel API 호출 허용
  • • 복잡한 상태관리 라이브러리 금지 (useState만 사용)
  • • WebSocket/실시간 통신 금지 (HTTP 요청만 사용)

프롬프트 레시피

아래 프롬프트를 순서대로 AI에게 보내면 됩니다. 복사 버튼을 눌러 바로 사용하세요.

1
Step 1
mkdir fullstack-click-counter && cd fullstack-click-counter

이 폴더에서 풀스택 카운터 앱 프로젝트를 시작합니다.

먼저 Laravel 13.x API 백엔드를 설정합니다:

1. `backend` 폴더에 Laravel 13 프로젝트 생성:
   ```bash
   composer create-project laravel/laravel:^13.0 backend
   cd backend
   ```

2. SQLite 데이터베이스 설정:
   - `.env` 파일에서 DB 설정 변경:
     ```
     DB_CONNECTION=sqlite
     # DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD 주석 처리
     ```
   - `database/database.sqlite` 빈 파일 생성:
     ```bash
     touch database/database.sqlite
     ```

3. 카운터 모델 및 마이그레이션 생성:
   ```bash
   php artisan make:model Counter -m
   ```

4. `database/migrations/xxxx_create_counters_table.php` 마이그레이션 파일 수정:
   ```php
   public function up()
   {
       Schema::create('counters', function (Blueprint $table) {
           $table->id();
           $table->integer('count')->default(0);
           $table->timestamps();
       });
   }
   ```

5. 마이그레이션 실행 및 초기 데이터 생성:
   ```bash
   php artisan migrate
   php artisan tinker
   # Tinker에서 실행:
   App\Models\Counter::create(['count' => 0]);
   exit
   ```

6. API 컨트롤러 생성:
   ```bash
   php artisan make:controller Api/CounterController
   ```

7. `app/Http/Controllers/Api/CounterController.php` 작성:
   ```php
   <?php

   namespace App\Http\Controllers\Api;

   use App\Http\Controllers\Controller;
   use App\Models\Counter;
   use Illuminate\Http\Request;

   class CounterController extends Controller
   {
       public function show()
       {
           $counter = Counter::first();
           return response()->json(['count' => $counter->count]);
       }

       public function increment()
       {
           $counter = Counter::first();
           $counter->increment('count');
           return response()->json(['count' => $counter->count]);
       }

       public function reset()
       {
           $counter = Counter::first();
           $counter->update(['count' => 0]);
           return response()->json(['count' => 0]);
       }
   }
   ```

8. `routes/api.php`에 라우트 추가:
   ```php
   use App\Http\Controllers\Api\CounterController;

   Route::get('/counter', [CounterController::class, 'show']);
   Route::post('/counter/increment', [CounterController::class, 'increment']);
   Route::post('/counter/reset', [CounterController::class, 'reset']);
   ```

9. CORS 설정 (`config/cors.php` 확인 및 필요시 수정):
   ```php
   'paths' => ['api/*'],
   'allowed_origins' => ['http://localhost:5173'], // React Vite 기본 포트
   'allowed_methods' => ['*'],
   'allowed_headers' => ['*'],
   ```

10. Laravel 개발 서버 실행:
    ```bash
    php artisan serve
    # http://127.0.0.1:8000 에서 실행됨
    ```

완료되면 `http://127.0.0.1:8000/api/counter`에 접속하여 `{"count":0}` JSON 응답이 나오는지 확인하세요.

기대 결과: Laravel API 서버가 http://127.0.0.1:8000에서 실행되고, SQLite DB에 counters 테이블이 생성되며, /api/counter GET 엔드포인트가 정상 작동하여 JSON 응답을 반환합니다.

2
Step 2
이제 React 19 프론트엔드를 설정합니다.

1. 프로젝트 루트 폴더(`fullstack-click-counter`)로 돌아가서 `frontend` 폴더에 Vite + React 프로젝트 생성:
   ```bash
   cd ..
   npm create vite@latest frontend -- --template react
   cd frontend
   npm install
   ```

2. Tailwind CSS 설치 및 설정:
   ```bash
   npm install -D tailwindcss postcss autoprefixer
   npx tailwindcss init -p
   ```

3. `tailwind.config.js` 수정:
   ```js
   export default {
     content: [
       "./index.html",
       "./src/**/*.{js,ts,jsx,tsx}",
     ],
     theme: {
       extend: {},
     },
     plugins: [],
   }
   ```

4. `src/index.css`에 Tailwind directives 추가:
   ```css
   @tailwind base;
   @tailwind components;
   @tailwind utilities;
   ```

5. `src/App.jsx` 파일을 다음과 같이 작성:
   ```jsx
   import { useState, useEffect } from 'react';

   const API_BASE_URL = 'http://127.0.0.1:8000/api';

   function App() {
     const [count, setCount] = useState(0);
     const [loading, setLoading] = useState(false);
     const [error, setError] = useState(null);

     // 페이지 로드 시 현재 카운트 가져오기
     useEffect(() => {
       fetchCount();
     }, []);

     const fetchCount = async () => {
       try {
         setLoading(true);
         const response = await fetch(`${API_BASE_URL}/counter`);
         if (!response.ok) throw new Error('카운트를 불러올 수 없습니다.');
         const data = await response.json();
         setCount(data.count);
         setError(null);
       } catch (err) {
         setError(err.message);
       } finally {
         setLoading(false);
       }
     };

     const handleIncrement = async () => {
       try {
         setLoading(true);
         const response = await fetch(`${API_BASE_URL}/counter/increment`, {
           method: 'POST',
         });
         if (!response.ok) throw new Error('카운트 증가 실패');
         const data = await response.json();
         setCount(data.count);
         setError(null);
       } catch (err) {
         setError(err.message);
       } finally {
         setLoading(false);
       }
     };

     const handleReset = async () => {
       if (!confirm('정말 카운트를 초기화하시겠습니까?')) return;
       try {
         setLoading(true);
         const response = await fetch(`${API_BASE_URL}/counter/reset`, {
           method: 'POST',
         });
         if (!response.ok) throw new Error('카운트 초기화 실패');
         const data = await response.json();
         setCount(data.count);
         setError(null);
       } catch (err) {
         setError(err.message);
       } finally {
         setLoading(false);
       }
     };

     return (
       <div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center p-4">
         <div className="bg-white rounded-2xl shadow-xl p-8 max-w-md w-full">
           <h1 className="text-3xl font-bold text-center text-indigo-600 mb-8">
             풀스택 카운터 앱
           </h1>

           <div className="text-center mb-8">
             <div className="text-7xl font-bold text-gray-800 mb-2">
               {count}
             </div>
             <p className="text-gray-500">총 클릭 수</p>
           </div>

           {error && (
             <div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-4">
               {error}
             </div>
           )}

           <div className="space-y-3">
             <button
               onClick={handleIncrement}
               disabled={loading}
               className="w-full bg-indigo-600 hover:bg-indigo-700 disabled:bg-indigo-300 text-white font-semibold py-4 px-6 rounded-lg transition duration-200 text-lg"
             >
               {loading ? '처리 중...' : '클릭! 🎯'}
             </button>

             <button
               onClick={handleReset}
               disabled={loading}
               className="w-full bg-gray-200 hover:bg-gray-300 disabled:bg-gray-100 text-gray-700 font-semibold py-3 px-6 rounded-lg transition duration-200"
             >
               초기화
             </button>
           </div>

           <p className="text-center text-sm text-gray-500 mt-6">
             Laravel API + React로 만든 첫 풀스택 앱
           </p>
         </div>
       </div>
     );
   }

   export default App;
   ```

6. 개발 서버 실행:
   ```bash
   npm run dev
   ```

브라우저에서 http://localhost:5173 접속하여 카운터 앱이 정상 작동하는지 확인하세요. "클릭!" 버튼을 누르면 숫자가 증가하고, "초기화" 버튼을 누르면 0으로 리셋되어야 합니다.

기대 결과: React 프론트엔드가 http://localhost:5173에서 실행되고, Laravel API와 정상적으로 통신하여 카운트를 불러오고, 증가시키고, 초기화할 수 있습니다. 버튼 클릭 시 로딩 상태가 표시되고, 에러 발생 시 사용자 친화적 메시지가 표시됩니다.

3
Step 3
마지막으로 사용자 경험을 개선하고 보너스 기능을 추가합니다.

1. 클릭 애니메이션 효과 추가: 숫자가 증가할 때 펄스 애니메이션 적용

`src/App.jsx` 수정:

```jsx
import { useState, useEffect } from 'react';

const API_BASE_URL = 'http://127.0.0.1:8000/api';

function App() {
  const [count, setCount] = useState(0);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [isPulsing, setIsPulsing] = useState(false);

  useEffect(() => {
    fetchCount();
  }, []);

  const fetchCount = async () => {
    try {
      setLoading(true);
      const response = await fetch(`${API_BASE_URL}/counter`);
      if (!response.ok) throw new Error('카운트를 불러올 수 없습니다.');
      const data = await response.json();
      setCount(data.count);
      setError(null);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  const handleIncrement = async () => {
    try {
      setLoading(true);
      const response = await fetch(`${API_BASE_URL}/counter/increment`, {
        method: 'POST',
      });
      if (!response.ok) throw new Error('카운트 증가 실패');
      const data = await response.json();
      setCount(data.count);
      setError(null);
      
      // 펄스 애니메이션 트리거
      setIsPulsing(true);
      setTimeout(() => setIsPulsing(false), 600);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  const handleReset = async () => {
    if (!confirm('정말 카운트를 초기화하시겠습니까?')) return;
    try {
      setLoading(true);
      const response = await fetch(`${API_BASE_URL}/counter/reset`, {
        method: 'POST',
      });
      if (!response.ok) throw new Error('카운트 초기화 실패');
      const data = await response.json();
      setCount(data.count);
      setError(null);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  // 클릭 수에 따른 칭호 계산
  const getBadge = () => {
    if (count >= 1000) return { text: '🏆 클릭 마스터', color: 'text-yellow-600' };
    if (count >= 500) return { text: '⭐ 클릭 전문가', color: 'text-purple-600' };
    if (count >= 100) return { text: '🎖️ 초보 클리커', color: 'text-blue-600' };
    if (count >= 10) return { text: '🌱 클릭 입문자', color: 'text-green-600' };
    return { text: '🐣 클릭 새싹', color: 'text-gray-600' };
  };

  const badge = getBadge();

  return (
    <div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center p-4">
      <div className="bg-white rounded-2xl shadow-xl p-8 max-w-md w-full">
        <h1 className="text-3xl font-bold text-center text-indigo-600 mb-8">
          풀스택 카운터 앱
        </h1>

        <div className="text-center mb-8">
          <div 
            className={`text-7xl font-bold text-gray-800 mb-2 transition-all duration-300 ${
              isPulsing ? 'scale-125 text-indigo-600' : 'scale-100'
            }`}
          >
            {count}
          </div>
          <p className="text-gray-500 mb-3">총 클릭 수</p>
          <div className={`text-lg font-semibold ${badge.color}`}>
            {badge.text}
          </div>
        </div>

        {error && (
          <div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-4">
            {error}
          </div>
        )}

        <div className="space-y-3">
          <button
            onClick={handleIncrement}
            disabled={loading}
            className="w-full bg-indigo-600 hover:bg-indigo-700 disabled:bg-indigo-300 text-white font-semibold py-4 px-6 rounded-lg transition duration-200 text-lg transform active:scale-95"
          >
            {loading ? '처리 중...' : '클릭! 🎯'}
          </button>

          <button
            onClick={handleReset}
            disabled={loading}
            className="w-full bg-gray-200 hover:bg-gray-300 disabled:bg-gray-100 text-gray-700 font-semibold py-3 px-6 rounded-lg transition duration-200"
          >
            초기화
          </button>
        </div>

        <div className="mt-6 pt-6 border-t border-gray-200">
          <p className="text-center text-sm text-gray-500">
            Laravel 13 API + React 19로 만든 첫 풀스택 앱
          </p>
          <p className="text-center text-xs text-gray-400 mt-2">
            다음 칭호까지: {1000 - (count % 1000)} 클릭
          </p>
        </div>
      </div>
    </div>
  );
}

export default App;
```

2. 테스트:
   - 여러 번 클릭하여 숫자가 증가할 때 애니메이션이 작동하는지 확인
   - 10회, 100회, 500회, 1000회 클릭 시 칭호가 바뀌는지 확인
   - 초기화 버튼으로 0으로 리셋되는지 확인
   - 브라우저를 새로고침해도 카운트가 유지되는지 확인 (DB에 저장됨)

3. (선택) README.md 작성:
   프로젝트 루트에 `README.md` 파일을 생성하여 프로젝트 설명, 실행 방법, 기술 스택을 문서화하세요.

완료! 첫 Laravel + React 풀스택 앱을 성공적으로 만들었습니다. 🎉

기대 결과: 클릭 시 숫자가 펄스 애니메이션과 함께 증가하고, 클릭 수에 따라 칭호(배지)가 표시됩니다. 다음 칭호까지 남은 클릭 수도 표시됩니다. 브라우저 새로고침 후에도 카운트가 유지됩니다.

제출된 작품 (0)

로그인 후 제출

아직 제출된 작품이 없습니다. 첫 번째 도전자가 되어보세요!

댓글 (0)

로그인 후 댓글을 남길 수 있습니다.
아직 댓글이 없습니다. 첫 번째 댓글을 남겨보세요!