프로젝트 소개
본 프로젝트는 Wordle과 유사한 단어 추측 게임으로, Next.js를 기반으로 구축되었습니다. 플레이어는 주어진 단어를 6번의 시도 안에 맞춰야 하며, 3글자에서 8글자까지 다양한 길이의 단어를 지원합니다. 초기에는 단순히 Wordle과 차별화된 요소를 추가하는 데 목적을 두었으나, 개발 과정에서 다양한 기술적 고민과 구현을 통해 흥미로운 게임으로 발전하게 되었습니다.
주말 동안 집중 개발하여 빠르게 출시한 후 자연스러운 트래픽 흐름을 관찰할 수 있었습니다. 특히 약 50%의 자연 검색 트래픽과 40%의 직접 방문 트래픽을 기록하여 안정적인 사용자 기반을 형성하는 모습을 확인했습니다.
기술 스택
주요 프레임워크
- React 18.3.1 - 사용자 인터페이스 구축
- Next.js 14.2.4 - 서버 사이드 렌더링 및 API 라우팅
- TypeScript - 타입 안전성 확보
- Tailwind CSS - 유틸리티 기반 스타일링
UI 컴포넌트 및 애니메이션
- Radix UI - 접근성 고려된 모달, 토스트 컴포넌트
- Lucide React - 아이콘 라이브러리
- Canvas Confetti - 성공 시 축하 효과
유틸리티 라이브러리
- SWR - 데이터 페칭 및 캐싱
- Zod - 런타임 데이터 검증
- nspell - 단어 철자 검사
프로젝트 구조
word-game/
├── src/
│ ├── app/
│ │ ├── api/
│ │ │ ├── hint-generator/
│ │ │ ├── word-validator/
│ │ │ └── word-picker/
│ │ ├── layout.tsx
│ │ └── page.tsx
│ ├── components/
│ │ ├── core/
│ │ ├── puzzle-grid.tsx
│ │ ├── input-pad.tsx
│ │ └── outcome-dialog.tsx
│ ├── resources/
│ │ └── dictionary.ts
│ ├── utilities/
│ │ ├── client-api.ts
│ │ └── helpers.ts
│ └── assets/
└── public/
핵심 기능 구현 상세
게임 상태 관리
React 훅을 활용하여 복잡한 게임 상태를 효율적으로 관리합니다.
const [letterCount, setLetterCount] = useState(5);
const [boardState, setBoardState] = useState<string[]>([]);
const [activePosition, setActivePosition] = useState(-1);
const [secretWord, setSecretWord] = useState('');
const [comparisonOutcomes, setComparisonOutcomes] = useState<string[]>([]);
const [tileStyles, setTileStyles] = useState<string[]>([]);
단어 선택 시스템
단일톤 패턴과 캐싱을 적용한 단어 선택기를 구현하여 동일 단어의 반복 선택을 방지합니다.
class VocabularySelector {
private static selectorInstance: VocabularySelector;
private wordPoolCache: Map<number, string[]> = new Map();
private selectedTracker: Map<number, Set<number>> = new Map();
private shuffledOrders: Map<number, number[]> = new Map();
public selectRandomWord(desiredLength: number): string {
if (!this.shuffledOrders.has(desiredLength)) {
const wordArray = this.fetchWordList(desiredLength);
const order = this.shuffleArray([...Array(wordArray.length).keys()]);
this.shuffledOrders.set(desiredLength, order);
}
const orderArray = this.shuffledOrders.get(desiredLength)!;
const usedSet = this.selectedTracker.get(desiredLength) || new Set();
let selectionIndex: number;
do {
const nextIndex = (this.selectedTracker.get(desiredLength)?.size || 0) % orderArray.length;
selectionIndex = orderArray[nextIndex];
} while (usedSet.has(selectionIndex));
usedSet.add(selectionIndex);
this.selectedTracker.set(desiredLength, usedSet);
return this.fetchWordList(desiredLength)[selectionIndex];
}
private shuffleArray<T>(array: T[]): T[] {
const shuffled = [...array];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + Village1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled;
}
}
단어 비교 알고리즘
const compareWords = (attempt: string, answer: string): string[] => {
const outcome = new Array(attempt.length).fill('absent');
const letterInventory = new Map<string, number>();
for (let idx = 0; idx < attempt.length; idx++) {
if (attempt[idx] === answer[idx]) {
outcome[idx] = 'exact';
} else {
const currentLetter = answer[idx];
letterInventory.set(currentLetter, (letterInventory.get(currentLetter) || 0) + 1);
}
}
for (let idx = 0; idx < attempt.length; idx++) {
if (outcome[idx] !== 'exact') {
const attemptedLetter = attempt[idx];
if ((letterInventory.get(attemptedLetter) || 0) > 0) {
outcome[idx] = 'present';
letterInventory.set(attemptedLetter, letterInventory.get(attemptedLetter)! - 1);
}
}
}
return outcome;
};
게임 보드 컴포넌트
interface PuzzleGridProps {
boardData: string[];
gridColumns: number;
highlightedCell: number;
cellStatusClasses: string[];
rotatingRows: Set<number>;
}
export const PuzzleGrid: React.FC<PuzzleGridProps> = ({
boardData,
gridColumns,
highlightedCell,
cellStatusClasses,
rotatingRows
}) => {
const gridStyle = `grid grid-cols-${gridColumns} gap-2 mb-8`;
return (
<div className={gridStyle}>
{boardData.map((cellValue, index) => {
const statusClass = cellStatusClasses[index];
const rowIndex = Math.floor(index / gridColumns);
const isRotating = rotatingRows.has(rowIndex);
return (
<div
key={index}
className={`
w-14 h-14 flex items-center justify-center text-2xl font-bold rounded-md border-2
${statusClass === 'exact' ? 'bg-emerald-500 text-white border-emerald-600' :
statusClass === 'present' ? 'bg-amber-500 text-white border-amber-600' :
statusClass === 'absent' ? 'bg-slate-400 text-white border-slate-500' :
'bg-white border-slate-200'}
${isRotating ? 'animate-[flip_0.6s_ease-in-out]' : ''}
`}
style={{
animationDelay: isRotating ? `${(index % gridColumns) * 100}ms` : '0ms'
}}
>
{cellValue}
</div>
);
})}
</div>
);
};
입력 패드 컴포넌트
interface InputKeyProps {
character: string;
isConfirmed: boolean;
isEliminated: boolean;
onKeyPress: (key: string) => void;
}
const InputKey = React.memo<InputKeyProps>(({
character,
isConfirmed,
isEliminated,
onKeyPress
}) => {
return (
<button
onClick={() => onKeyPress(character)}
className={`
min-w-[3.5rem] h-14 rounded-lg font-semibold transition-all duration-150
${isConfirmed ? 'bg-emerald-500 text-white shadow-lg' :
isEliminated ? 'bg-slate-400 text-white' :
'bg-white text-slate-800 hover:bg-slate-50 active:scale-95 shadow'}
`}
aria-label={`Press ${character}`}
>
{character}
</button>
);
});
InputKey.displayName = 'InputKey';
데이터 구조
단어 사전
export const WORD_DATABASE: Record<number, string[]> = {
3: ['cat', 'dog', 'sun', /* ... 500+ 단어 */],
4: ['door', 'book', 'tree', /* ... 500+ 단어 */],
5: ['apple', 'house', 'water', /* ... 500+ 단어 */],
6: ['garden', 'window', 'planet', /* ... 500+ 단어 */],
7: ['journey', 'evening', 'library', /* ... 500+ 단어 */],
8: ['mountain', 'birthday', 'language', /* ... 500+ 단어 */]
};
API 설계
엣지 런타임 API
Next.js 엣지 런타임을 활용하여 낮은 지연 시간의 API 응답을 제공합니다.
export const runtime = 'edge';
export const dynamic = 'force-dynamic';
export async function GET(request: NextRequest): Promise<NextResponse> {
const searchParams = request.nextUrl.searchParams;
const requestedLength = parseInt(searchParams.get('length') || '5');
const wordSelector = VocabularySelector.getInstance();
const selectedWord = wordSelector.selectRandomWord(requestedLength);
return NextResponse.json(
{ word: selectedWord, length: requestedLength },
{ status: 200 }
);
}
성능 및 경험 최적화
애니메이션 효과
- 타일 플립 애니메이션: CSS 키프레임을 활용한 3D 변환 효과
- 키 입력 피드백: 버튼 인터랙션 시 미세한 시각적 변화
- 성공 축하 Canvas Confetti 라이브러리를 통한 입자 효과
반응형 디자인
- 모바일 장치에 최적화된 입력 패드 레이아웃
- 화면 크기별 동적 그리드 조정
- 터치 인터랙션 개선
렌더링 최적화
- React.memo를 통한 불필요한 리렌더링 방지
- useCallback 및 useMemo 훅을 통한 계산 결과 캐싱
- 단어 선택기의 인메모리 캐시 활용
검색 엔진 최적화
export const metadata: Metadata = {
title: "무제한 단어 퍼즐 - 6번의 시도로 단어를 맞춰보세요",
description: "다양한 길이의 영어 단어를 추측하는 온라인 워드 게임",
keywords: "워드 게임, 단어 맞추기, 퍼즐, 영단어, 두뇌 게임",
openGraph: {
type: 'website',
locale: 'ko_KR',
},
};
게임 플로우
- 초기화: 원하는 단어 길이 선택 (3-8 글자)
- 목표 단어 생성: 사전에서 무작위 단어 선택
- 사용자 입력: 가상 키패드 또는 물리적 키보드로 글자 입력
- 입력 검증: 유효한 단어인지 확인
- 비교 분석: 글자 일치 여부 계산
- 시각적 피드백: 색상을 통한 힌트 제공 (초록색: 정확한 위치, 노란색: 존재하지만 다른 위치, 회색: 존재하지 않음)
- 게임 종료: 6번 시도 종료 또는 정답 맞춤
- 결과 표시: 결과 통계와 축하 효과 표시
기술적 특장점
- 현대적 기술 스택: 최신 React 생태계 도구 활용
- 성능 중심 설계 다계층 캐싱 및 렌더링 최적화
- 유지보수성 명확한 관심사 분리와 컴포넌트 구조
- 사용자 경험 부드러운 전환 애니메이션과 직관적 인터랙션
- 확장성: 모듈식 아키텍처로 향후 기능 추가 용이