뽀모도로 타이머, AI로 10분 만에 직접 만들어 쓰기
SVG 원형 프로그레스 바와 Web Audio API 비프음을 갖춘 뽀모도로 타이머를 직접 만들었습니다. 외부 파일 없이 소리를 내는 방법도 공유합니다.
2026년 5월 27일7분 읽기0 조회
?
#뽀모도로#타이머#AI코딩#바이브코딩#뚝딱공방#생산성
✦ 라이브 데모AI 프롬프트로 생성한 결과물
위 화면은 이 포스트의 프롬프트를 AI에 입력해서 생성한 샘플입니다
AI 프롬프트 공개
## 실제 사용한 프롬프트
---
### 1단계 — 기본 구조 요청
```
Next.js 14 + TypeScript + Tailwind로 뽀모도로 타이머 컴포넌트를 만들어줘.
요구사항:
- 'use client' 컴포넌트
- 3가지 모드: 집중(25분), 짧은 휴식(5분), 긴 휴식(15분)
- 시작/일시정지/리셋 버튼
- MM:SS 형태로 남은 시간 표시
- 완료된 뽀모도로 개수 표시
- 4번 완료 시 자동으로 긴 휴식으로 전환
- 다크모드 대응 (CSS 변수 사용)
```
---
### 2단계 — 원형 프로그레스 추가
```
뽀모도로 타이머에 원형 프로그레스 바를 추가해줘.
- SVG circle의 stroke-dashoffset을 사용
- 원 반지름: 80px
- 배경 원과 진행 원 두 겹으로 구성
- 진행 원은 mode별 색상 (집중: 초록, 짧은 휴식: 파랑, 긴 휴식: 보라)
- stroke-dashoffset 변화에 1초 linear 트랜지션
- 원 중앙에 MM:SS 텍스트와 모드 이모지 표시
```
---
### 3단계 — 추가 기능
```
뽀모도로 타이머에 다음을 추가해줘:
1. 타이머 완료 시 Web Audio API로 비프음 (외부 파일 없이 oscillator 사용)
2. 타이머 실행 중 document.title 업데이트 (예: "24:30 🍅 — 뽀모도로")
3. 타이머 종료 시 document.title을 원래 제목으로 복원
4. 완료된 뽀모도로를 🍅 이모지로 표시
```
---
💡 **팁**: SVG 원형 프로그레스는 개념 설명을 요청하고, 직접 계산식을 확인한 후 코드를 요청하면 더 정확합니다.
왜 뽀모도로 타이머를 직접 만들었나?
뽀모도로 기법은 간단합니다. 25분 집중, 5분 휴식을 반복하는 것이죠. 앱을 굳이 설치할 필요 없이, 브라우저 탭 하나로 쓸 수 있으면 더 편합니다.
기존 웹 뽀모도로 앱들은 기능이 너무 많거나, 불필요한 알림 권한을 요청하거나, 광고가 있습니다. 필요한 것만 갖춘 깔끔한 타이머를 직접 만들기로 했습니다.
라이브 데모
위의 타이머를 직접 사용해보세요.
- 🍅 집중 (25분) / ☕ 짧은 휴식 (5분) / 🌿 긴 휴식 (15분) 세 가지 모드
- 시작, 일시정지, 리셋 버튼으로 제어합니다
- 타이머 완료 시 간단한 알림음이 울립니다 (Web Audio API)
- 집중을 4번 완료하면 자동으로 긴 휴식으로 전환됩니다
- 브라우저 탭 제목에도 남은 시간이 표시됩니다
코드 핵심 포인트
1. SVG 원형 프로그레스 바
원형 진행 표시는 SVG의 stroke-dashoffset을 사용합니다.
const CIRCLE_R = 80
const CIRCLE_C = 2 * Math.PI * CIRCLE_R // 원의 둘레
// 진행률에 따라 dashoffset을 줄여나감
const dashOffset = CIRCLE_C * (1 - progress)
stroke-dasharray를 원의 둘레로 설정하고, stroke-dashoffset을 0에서 둘레까지 변화시키면 원이 채워지는 애니메이션이 됩니다.
2. Web Audio API로 알림음
외부 음원 파일 없이 브라우저의 Web Audio API로 비프음을 만듭니다.
function playBeep() {
const ctx = new AudioContext()
const osc = ctx.createOscillator()
const gain = ctx.createGain()
osc.connect(gain)
gain.connect(ctx.destination)
osc.frequency.value = 880 // A5 음
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.6)
osc.start()
osc.stop(ctx.currentTime + 0.6)
}
3. document.title 업데이트
타이머가 돌아가는 동안 브라우저 탭에 남은 시간을 표시합니다. 다른 탭에 있어도 시간을 확인할 수 있습니다.
useEffect(() => {
if (isRunning) {
document.title = `${mm}:${ss} 🍅 — 뽀모도로`
} else {
document.title = originalTitle // 원래 제목으로 복원
}
}, [isRunning, mm, ss])
마무리
단순한 타이머지만, SVG 애니메이션과 Web Audio API를 배울 수 있는 좋은 예제입니다. AI가 두 기술의 핵심 패턴을 한 번에 보여줬습니다.
다음 편 예고
다음 편은 색상 코드 변환기입니다. HEX → RGB → HSL → CMYK 변환 공식과 컬러 피커 UI를 다룹니다.
소스 코드
'use client'
import { useState, useEffect, useRef, useCallback } from 'react'
const MODES = {
focus: { label: '집중', emoji: '🍅', seconds: 25 * 60 },
short: { label: '짧은 휴식', emoji: '☕', seconds: 5 * 60 },
long: { label: '긴 휴식', emoji: '🌿', seconds: 15 * 60 },
}
const CIRCLE_R = 80
const CIRCLE_C = 2 * Math.PI * CIRCLE_R // 원의 둘레 ≈ 502.65
// Web Audio API 비프음
function playBeep() {
const ctx = new AudioContext()
const osc = ctx.createOscillator()
const gain = ctx.createGain()
osc.connect(gain)
gain.connect(ctx.destination)
osc.frequency.value = 880
gain.gain.setValueAtTime(0.3, ctx.currentTime)
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.6)
osc.start(ctx.currentTime)
osc.stop(ctx.currentTime + 0.6)
}
export function PomodoroTimer() {
const [mode, setMode] = useState('focus')
const [timeLeft, setTimeLeft] = useState(MODES.focus.seconds)
const [isRunning, setIsRunning] = useState(false)
const [pomodoroCount, setPomodoroCount] = useState(0)
const intervalRef = useRef(null)
const progress = (MODES[mode].seconds - timeLeft) / MODES[mode].seconds
const dashOffset = CIRCLE_C * (1 - progress)
const mm = String(Math.floor(timeLeft / 60)).padStart(2, '0')
const ss = String(timeLeft % 60).padStart(2, '0')
const handleComplete = useCallback(() => {
setIsRunning(false)
playBeep()
if (mode === 'focus') {
const newCount = pomodoroCount + 1
setPomodoroCount(newCount)
const nextMode = newCount % 4 === 0 ? 'long' : 'short'
setMode(nextMode)
setTimeLeft(MODES[nextMode].seconds)
} else {
setMode('focus')
setTimeLeft(MODES.focus.seconds)
}
}, [mode, pomodoroCount])
useEffect(() => {
if (isRunning) {
intervalRef.current = setInterval(() => {
setTimeLeft(prev => {
if (prev <= 1) { handleComplete(); return 0 }
return prev - 1
})
}, 1000)
} else {
clearInterval(intervalRef.current)
}
return () => clearInterval(intervalRef.current)
}, [isRunning, handleComplete])
return (
<div>
{/* 모드 버튼 */}
{Object.entries(MODES).map(([key, m]) => (
<button key={key} onClick={() => { setMode(key); setTimeLeft(m.seconds); setIsRunning(false) }}>
{m.emoji} {m.label}
</button>
))}
{/* SVG 원형 프로그레스 */}
<svg width="200" height="200" viewBox="0 0 200 200">
{/* 배경 원 */}
<circle cx="100" cy="100" r={CIRCLE_R} fill="none" strokeWidth="8" stroke="#e5e7eb" />
{/* 진행 원 */}
<circle
cx="100" cy="100" r={CIRCLE_R}
fill="none" strokeWidth="8" stroke="#22c55e"
strokeLinecap="round"
strokeDasharray={CIRCLE_C}
strokeDashoffset={dashOffset}
transform="rotate(-90 100 100)"
style={{ transition: 'stroke-dashoffset 1s linear' }}
/>
</svg>
{/* 시간 표시 */}
<p>{mm}:{ss}</p>
{/* 컨트롤 */}
<button onClick={() => setIsRunning(prev => !prev)}>
{isRunning ? '일시정지' : '시작'}
</button>
<button onClick={() => { setIsRunning(false); setTimeLeft(MODES[mode].seconds) }}>
리셋
</button>
{/* 뽀모도로 카운트 */}
<p>{'🍅'.repeat(pomodoroCount)}</p>
</div>
)
}2026년 5월 27일 게시