1. 트러블 슈팅
1.1. 링크를 통해 문제 공유
서버 없이 프론트엔드로만 만드는 앱이라서, 문제를 다른사람들과 공유하기 위해서는 링크를 통해 공유해야 합니다. 단순히 링크에 단어를 명시할 수도 있지만 정답이 링크에 노출되므로, 난독화가 필요합니다.
보안적으로 크게 중요한 내용은 아니므로, 단순히 난독화만 하면 될 것 같다고 생각하여 base64
형태로 인코딩하여 url을 만드는 방식을 사용했습니다.
JavaScript, TypeScript에서는 btoa
, atob
함수를 사용하여 base64 인코딩과 디코딩을 할 수 있습니다.
그리고, encodeURIComponent
와 decodeURIComponent
를 사용하여 URL에서 사용하기 안전한 형태로 변환하는 작업을 추가했습니다.
export const b64ToData = (b64?: string | null) => {
if (!b64) return null;
try {
return decodeURIComponent(atob(b64));
} catch (e) {
console.error(e);
return null;
}
};
export const toHash = (str?: string) => {
if (!str) return null;
try {
return btoa(encodeURIComponent(str));
} catch (e) {
console.error(e);
return null;
}
};
1.2. 게임 데이터를 어떻게 관리하는게 좋을까?
로그인을 하고 게임을 진행하는 것이 아니기 떄문에, 사용자가 브라우저를 닫고 다음에 다시 방문해도 동일한 URL로 접속하면 이전에 진행하던 게임을 다시 할 수 있어야 합니다.
브라우저를 닫아도 데이터가 저장이되어 있어야 하므로, localStrage
에 저장해야 합니다. 그리고 게임 진행 데이터를 전역 상태로 관리하면 rops drilling을 피할 수 있기 때문에 zustand
라이브러리를 사용하였습니다.
zustand
에는 persist
라는 미들웨어가 있어서, 전역 상태 값을 메모리에 뿐 아니라 localStorage
에 저장해주는 역할도 해줍니다.
// 미들웨어 없이 사용하는 경우
export const useGameDataStore = create<GameDataStoreState>()((set) => ({ ... }));
// 미들웨어를 사용하는 경우
export const useGameDataStore = create<GameDataStoreState>()(
persist(
(set) => ({ ... }),
{ name: 'gameDataStore' }, // localStorage key 이름
),
);
1.3. 게임과 비지니스 로직을 어떻게 관리할 것인가?
처음에는 게임의 비지니스 로직을 컴포넌트 내부에 넣어 개발했으나, 다른 컴포넌트에서도 사용하고 있어 코드 중복이 발생하고 코드가 나눠져있어 관리하기가 어려웠습니다.
그래서 게임과 관련된 비지니스 로직을 hook으로 만들어서 컴포넌트에서 가져다 사용할 수 있도록 개발했습니다.
export const useGame = () => {
const nav = useNavigate();
const totalGameData = useGameDataStore((state) => state.totalGameData);
const setGameData = useGameDataStore((state) => state.setGameData);
const gameHistories = useGameDataStore((state) => state.gameHistories);
const setGameHistories = useGameDataStore((state) => state.setGameHistories);
const getInitGameData = useCallback(...);
const getGameDataByWord = useCallback(...);
const startGame = useCallback(...); // 게임 시작
const updateData = useCallback(...); // 상태 업데이트
const finishGame = useCallback(...); // 게임 종료
return { getGameDataByWord, startGame, updateData, finishGame, gameHistories };
};
1.4. 플레이 시간 기록
사용자의 플레이 타임을 기록하기 위해, startTime와 endTime을 기록하여 플레이 시간을 계산하려 했습니다. 하지만, 사용자가 중간에 브라우저를 닫고 나중에 다시 게임을 즐길 수도 있기 때문에, 정확하지 않은 시간이 측정될 수 있었습니다.
결국에는 정확하게 play time을 측정하려면 1초마다 데이터를 저장하는게 좋겠다고 판단했고, useEffect와 setInterval
을 사용하여 전역 상태 값의 seconds 데이터를 1초마다 갱신하는 방법을 사용했습니다.
setInterval을 사용할때는 리소스 낭비를 막기 위해, useEffect의 cleanup 함수에서 clearInterval을 반드시 해줘야 합니다.
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const clear = useCallback(() => {
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
}, []);
useEffect(() => {
timerRef.current = setInterval(() => {
// 데이터 갱신
}, 1000)
return () => clear();
}, [clear]);
timer를 시작하거나 리셋 혹은 일시정지를 할 수도 있을것 같았고, 컴포넌트 내부에 timer를 두는 것보다 hook으로 빼서 관리하는게 좋을 것 같다고 판단하여, useGameTimer
라는 hook을 만들어서 관리했습니다.
export const useGameTimer = (initialSeconds = 0) => {
const [seconds, setSeconds] = useState<number>(initialSeconds);
const [isRunning, setIsRunning] = useState<boolean>(false);
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const clear = useCallback(() => {
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
}, []);
const startTimer = useCallback(() => {
setIsRunning(true);
}, []);
const pauseTimer = useCallback(() => {
clear();
setIsRunning(false);
}, [clear]);
const resetTimer = useCallback(() => {
setSeconds(0);
setIsRunning(true);
}, [initialSeconds]);
useEffect(() => {
if (isRunning && !timerRef.current) {
timerRef.current = setInterval(() => {
setSeconds((prev) => prev + 1);
}, 1000);
}
return () => clear();
}, [isRunning, startTimer, clear]);
return { seconds, isRunning, startTimer, pauseTimer, resetTimer };
};
만들어진 hook을 이용해서 사용하면 되므로, 컴포넌트 내부에서의 가독성이 높아져 코드가 깔끔해졌습니다.
1.5. 단어 길이에 대한 확장성고려
wordle 게임이 5자리의 단어로 진행하긴 하지만, 단어의 길이를 늘리거나 줄일 수 있도록 확장성을 고려하여 단어의 길이를 환경 변수로 분리하였습니다.
...
VITE_WORD_LENGTH=5
단어를 검증하는 부분은 정규식을 사용하여 /^[a-zA-Z]{5}$/
형태로 검증을 하였지만, 단어의 길이가 변경될 수 있으므로, 검증 로직을 new RegExp
형태로 변환했습니다.
new RegExp(`^[a-zA-Z]{${ENV.WORD_LENGTH}}$`).test(word)
환경 변수를 통해 여러 글자 단어를 지원할 수 있도록 하여, 다음 사진과 같이 앱의 확장성을 높였습니다.



지금은 환경 변수로 조작하여 확장성을 높였지만, 사용자가 원하는 길이로 단어를 변경 할 수 있게 기능으로 추가해도 괜찮을거 같습니다. (이건 추후 개선해볼 예정입니다)