꼬들 개선 프로젝트 - 뉴들 개발기 (2)

이번 글에서는 뉴들의 프론트엔드를 구현하는 과정에서 있었던 일들을 소개해보려고 한다.

사실 이번 프로젝트는 처음부터 모든 기능을 완벽히 설계해놓고 시작한 것이 아니었기 때문에 프론트엔드 작업을 하다가 다시 백엔드로 넘어가고, 다시 또 프론트로 돌아오는 식으로 작업 흐름이 유동적이었다.
그렇다 보니 구현 과정을 시간 순으로 정리하기보다는, 주요 기능 중심으로 나눠서 설명해 보겠다.


1. 프로젝트 구성

가장 먼저 뉴들의 기반이 될 깃허브 레포지토리를 포크해 프로젝트를 시작했다.

GitHub - roedoejet/AnyLanguage-Word-Guessing-Game: A word guessing game that can be modified and translated to your language!
A word guessing game that can be modified and translated to your language! - roedoejet/AnyLanguage-Word-Guessing-Game

꼬들 자체는 오픈소스로 공개된 프로젝트가 아니기 때문에 이 레포를 기반으로 한국어를 지원할 수 있도록 수정하고, 게임의 핵심 로직 및 UI를 꼬들과 최대한 유사하게 커스터마이징했다.
사실 뉴들도 기본적인 게임 구조나 디자인이 이 라이브러리와 크게 다르지 않았기 때문에 기반 코드를 수정하는 작업은 생각보다 어렵지 않았다

그다음 단계는 프로젝트에 불필요한 기능들을 정리하는 작업이었다.

기반 코드에서는 i18n을 활용해 다국어 지원이 가능하도록 구성되어 있었지만,
이번 프로젝트에서는 오직 한국어만을 대상으로 하므로 이 기능은 전부 제거했다.

또한, 원본 코드에서는 정답 단어 목록이 프론트엔드에 하드코딩되어 있었는데,
뉴들에서는 이 데이터를 백엔드에서 따로 제공할 계획이었기 때문에 해당 관련 코드도 전부 정리했다.

이제 남은 과제는 정답 단어를 어떤 방식으로 제공할지를 결정하는 것이었다.

가장 단순한 방법은 입력 가능한 단어 목록과 함께 정답 단어도 페이지 로드 시 프론트로 전달하는 방식이다.
하지만 이 방식은 리버스 엔지니어링을 통해 정답이 너무 쉽게 노출될 수 있다는 단점이 있었다.
실제로 워들도 이 방식 때문에 정답을 하루치 이상 미리 볼 수 있는 문제가 있었고,
뉴들에서는 이런 구조를 그대로 답습하고 싶지 않았다.

그래서 채택한 방식은 다음과 같다:
사용자가 단어를 입력해 제출할 때마다 서버로 POST 요청을 보내 해당 단어가 정답인지 여부만 판단하는 구조다.

이 방식은 정답 단어를 절대 클라이언트에 노출하지 않기 때문에 게임의 공정성과 보안 측면에서 훨씬 안전한 구조라고 판단했다.


2. 메인 기능

꼬들에는 기본적으로 6글자 단어를 맞히는 일반 게임 모드 '꼬들',
그리고 12글자 단어를 맞히는 확장 모드 '꼬오오오들'이 존재한다.

뉴들에서는 이 두 모드를 그대로 지원하며, 여기에 더해 4글자 단어를 맞히는 '뉻'을 추가로 제공한다.

한 가지 구조적인 문제도 있었다.
꼬들에서는 각 게임 모드를 별도로 빌드하여 독립된 사이트로 운영하고 있기 때문에,
모드 전환 시 완전히 독립적인 페이지로 작동한다.

하지만 뉴들에서는 확장성을 위해 React 라우팅을 활용해 한 페이지 내에서 모든 모드를 전환할 수 있도록 구성했다.
이렇게 하면 코드를 재사용할 수 있고, 기능을 추가할 때도 훨씬 유연하게 대응할 수 있다는 장점이 있다.

그러나 페이지 자체를 새로 불러오지 않고 URL(path)만 바뀌는 구조이기 때문에 게임 상태와 관련된 모든 state를 라우팅 변경 시 초기화하고 다시 구성해줘야 했다.

이를 해결하기 위해 아래와 같은 형태로 게임 모드별 설정값을 담은 gameConfig 객체를 만들었다.

export const gameConfig: Record<GamePath, { title: string; tries: number; wordLength: number }> = {
    normal: { title: '뉴들', tries: 6, wordLength: 6 },
    long: { title: '뉴우우우우들', tries: 6, wordLength: 12 },
    short: { title: '뉻', tries: 6, wordLength: 4 }
}

각 모드마다 제목, 단어 길이, 최대 추측 횟수 등의 설정값을 정의해두고,
라우팅이 변경될 때마다 이 값을 동적으로 불러와 적용할 수 있도록 했다.

React 컴포넌트 내부에서는 다음과 같은 다양한 상태 값들을 관리한다:

const [currentGuess, setCurrentGuess] = useState<Array<string>>([])
const [isGameWon, setIsGameWon] = useState(false)
const [guesses, setGuesses] = useState<GuessInfo[]>([])

이 상태들은 모두 게임 진행 중인 단어, 승리 여부, 지금까지의 추측 기록 등과 관련된 값들이다.
useLocation 훅을 통해 현재 경로(path)를 받아오고 경로가 변경될 때마다 관련 상태를 초기화하고, 해당 모드에 맞는 설정을 다시 적용하도록 로직을 구성했다.

덕분에 하나의 컴포넌트 내에서 동적으로 게임 모드를 전환할 수 있는 구조를 완성할 수 있었다.
향후 모드를 추가하거나 로직을 확장할 때도, 이 구조 덕분에 코드 수정을 최소화할 수 있게 되었다.


3. 커스텀 단어 생성

꼬들에서는 사용자가 직접 단어를 만들어 게임을 생성할 수 있는 기능도 제공하고 있지만 몇 가지 제약이 존재한다.
글자 수가 6자 또는 12자로 제한되어 있고, 생성된 문제는 URL을 통해서만 공유 가능하기 때문에 불특정 다수가 자유롭게 이용하기에는 불편함이 있었다.

뉴들에서는 이러한 한계를 개선하기 위해 사용자가 만들 수 있는 단어의 길이를 최소 4자에서 최대 15자까지 설정 가능하도록 변경했다.
또한, 단순한 URL 공유 외에 공개 여부를 설정할 수 있는 필드를 추가했다.

비공개로 설정된 문제는 기존처럼 URL을 통해서만 접근 가능하고, 공개로 설정된 문제는 서버에 등록되어 API를 통해 모든 사용자에게 제공되며 게시판 형태로 열람 및 참여가 가능하도록 구성했다.

위 이미지와 같이, 게시판에서는 다음과 같은 정렬 및 검색 기능을 제공한다:

  • 문제 등록일 기준 정렬
  • 풀이 횟수 기준 정렬
  • 단어 수 필터링 (4~15자)
  • 제작자 이름 검색 기능

특히 제작자 검색 기능의 경우, 일반적인 텍스트 검색과는 방식이 다르다.
뉴들은 글자를 초성, 중성, 종성 단위로 분해해 입력을 처리하는 구조를 가지고 있기 때문에 검색에서도 동일한 로직을 적용해야 했다.

이를 위해 기존에 문제 입력에 사용하던 가상 키보드 객체를 그대로 재사용하여,
제작자 검색창도 자모 단위 입력/비교가 가능한 방식으로 구현했다.


4. 백오피스

불특정 다수가 문제를 등록하고 열람할 수 있는 게시판 구조에서는 언제든지 악성 사용자나 부적절한 문제가 등록될 수 있는 위험이 존재한다.
따라서 이를 적절히 통제하고 관리할 수 있는 시스템도 함께 설계해야 했다.

뉴들에서는 별도의 로그인 기능 없이도 서비스를 이용할 수 있도록 구성했기 때문에,
브라우저 단위로 고유한 UUID를 생성하고 접속 IP와 함께 저장하여 사용자 식별에 활용하도록 했다.

이 과정에서 IP 주소는 사용자에게 사실상 개인 식별자 역할을 하게 되므로, 개인정보 처리방침을 별도로 작성하여 고지하는 것도 필수적이었다.

커스텀 단어 목록 및 관리, 특정 사용자/IP 차단, 감사 로그 확인 기능을 두어 관리가 용이하게 제작하였다.


마치며

이번 글에서는 뉴들의 프론트엔드 구현 과정을 중심으로 주요 기능들과 구조적인 결정 사항들을 소개해보았다.
초기 셋업부터 모드 전환, 사용자 문제 생성, 게시판 구성, 관리 기능까지 전반적인 흐름을 다뤘다.

다음 글에서는 백엔드 중심의 구현 과정과 함께 API 설계, 단어 제공 방식, 데이터 구조, 그리고 보안 고려 사항 등 뉴들의 서버 사이드 로직에 대해 자세히 풀어볼 예정이다.