풀스택 카운터 앱 - 실시간 클릭 집계
fullstack-click-counter
문제 설명
프로젝트 개요
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에게 보내면 됩니다. 복사 버튼을 눌러 바로 사용하세요.
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 응답을 반환합니다.
이제 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와 정상적으로 통신하여 카운트를 불러오고, 증가시키고, 초기화할 수 있습니다. 버튼 클릭 시 로딩 상태가 표시되고, 에러 발생 시 사용자 친화적 메시지가 표시됩니다.
마지막으로 사용자 경험을 개선하고 보너스 기능을 추가합니다.
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)
로그인 후 제출아직 제출된 작품이 없습니다. 첫 번째 도전자가 되어보세요!