์ต๊ทผ์ ํ
์คํธ ์ฝ๋์ ๋ํด์ ๊ณต๋ถํด๋ณด๋ฉด์ ํ
์คํธํ๊ธฐ ์ข์ ์ฝ๋๊ฐ ์ ์ค๊ณ๋ ์ฝ๋๋ผ๋๊ฑธ ๋ฐฐ์ ๋ค.
๊ทธ๋์ ์ง๊ธ๊น์ง ์ค๊ตฌ๋๋ฐฉ ์์ฑ๋ ์ฝ๋๋ค์ ์ ๊ฒํ๊ณ ๋ฆฌํฉํ ๋ง(ํด๋ก๋๊ฐใ
) ํด๋ดค๋ค.
๋ฆฌํฉํ ๋ง ๋ด์ฉ
์์ฝ
- ์ ํฌ ๋ก์ง ๋ถ๋ฆฌ :
BattleField.tsx->battle.ts(๋ค๋ฅธ ๊ธ์์ ๋ค๋ฃฌ๋ค) - ๋ ๋ฒจ์
๋ก์ง ๋ถ๋ฆฌ :
useGameStore.ts->battle.ts(๋ค๋ฅธ ๊ธ์์ ๋ค๋ฃฌ๋ค) - storage ์ถ์ํ ๊ณ์ธต ์ถ๊ฐ :
storage.tsํ์ผ ์ถ๊ฐ - encounter ์์กด์ฑ ์ฃผ์
:
encounter.ts
encounter ์์กด์ฑ ์ฃผ์
// before
export function checkEncounter(): boolean {
const { incrementStepCount, incrementTotalEncounters, setEncounteredPkmon } =
useGameStore.getState(); // ํ์ดํธ ์ปคํ๋ง, Store์ ์ง์ ์์กด์ค
incrementStepCount();
const shouldEncounter = Math.random() < 1 / ENCOUNTER_RATE; // ํ์ดํธ ์ปคํ๋ง
if (shouldEncounter) {
incrementTotalEncounters();
const wildPkmon = generateRandomPkmon();
setEncounteredPkmon(wildPkmon);
console.log("[Encounter] Wild Pkmon appeared!", wildPkmon);
}
return shouldEncounter;
}
// after
export function checkEncounter(
actions?: EncounterActions,
encounterRate: number = ENCOUNTER_RATE,
randomFn: () => number = Math.random
): boolean {
// actions๊ฐ ์์ผ๋ฉด ์ค์ store ์ฌ์ฉ (๊ธฐ๋ณธ ๋์)
const { incrementStepCount, incrementTotalEncounters, setEncounteredPkmon } =
actions || useGameStore.getState(); // ํ
์คํธ์์๋ action ์ธ์๋ฅผ ๋ฃ์ด ๊ฒ์ฆ ๊ฐ๋ฅ
incrementStepCount();
const shouldEncounter = shouldTriggerEncounter(encounterRate, randomFn);
if (shouldEncounter) {
incrementTotalEncounters();
const wildPkmon = generateRandomPkmon(PKMON_SPECIES, randomFn);
setEncounteredPkmon(wildPkmon);
console.log("[Encounter] Wild Pkmon appeared!", wildPkmon);
}
return shouldEncounter;
}
ํจ์ ๋ด๋ถ์์ useGameStore์ Math.random() < 1์ ์ง์ ์ฌ์ฉํ๋๋ฐ, (๋ด๋ถ์์ ์ง์ ์์กด์ฑ ์์ฑ)
์ด๋ฅผ ๋์ ํด ์ธ๋ถ๋ก๋ถํฐ ํ๋ผ๋ฏธํฐ๋ก ์ ๋ฌ์ ๋ฐ๋๋ก ํด์ ์์กด์ฑ ์ฃผ์
์ด๋ผ๊ณ ํ๋ค.
ํ ์คํธ ์ฉ์ด์ฑ
// encounter.test.ts
const mockActions: EncounterActions = {
incrementStepCount: vi.fn(),
incrementTotalEncounters: vi.fn(),
setEncounteredPkmon: vi.fn(),
};
const mockRandom = vi
.fn()
.mockReturnValue(0.05) // encounter ๋ฐ์, ๊ธฐ๋ณธ๊ฐ
.mockReturnValueOnce(0.05)
.mockReturnValueOnce(0.0) // ์ข
์ ํ
.mockReturnValueOnce(0.0); // ๋ ๋ฒจ ์ ํ
const result = checkEncounter(mockActions, 10, mockRandom);
expect(result).toBe(true);
expect(mockActions.incrementStepCount).toHaveBeenCalledTimes(1);
expect(mockActions.incrementTotalEncounters).toHaveBeenCalledTimes(1);
expect(mockActions.setEncounteredPkmon).toHaveBeenCalledTimes(1);
// EncounterControls.tsx
<button
onClick={() => checkEncounter()}
disabled={!encounterEnabled}
className="w-full bg-blue-600 hover:bg-blue-700 px-3 py-1.5 rounded
disabled:bg-gray-600 disabled:cursor-not-allowed disabled:opacity-50"
>
ํ
์คํธ์์๋ ๊ฐ ์ธ์๋ค(mockActions, mockRandom) ์ ๋ฃ์ด์ ํธ์ถํ๋ค.
mockActions : ์คํ์ด ํจ์๋ค์ด ๋ค์ด์์ด์ expect ๋งค์ฒ๋ก ํ์ธํ ์ ์๋ค.
mockRandom : Stub์ด๋ผ๊ณ ๋ถ๋ฅด๋, ์ค์ ํจ์๋ฅผ ๋์ฒดํ๋ ์ญํ .
Stub ํจ์
์ค์ ํจ์(์ฌ๊ธฐ์๋ randomFn = Math.random()) ๋ฅผ ๋์ ํ๋ค.
๋ฏธ๋ฆฌ ์ ํด์ง ๊ฐ์ ๋ฐํํ๋ค. ์ฆ ์์ mockRandom์ ๋ณด๋ฉด 0.05 -> 0.0 -> 0.0์ ๋ฐํํ๋๊ฒ์ ๋ณผ ์ ์๋ค. (mockReturnValue๋ ๊ธฐ๋ณธ๊ฐ)
์ด์ checkEncounter()์์ mockRandom์ด ์ธ์ ์ด๋์ ํธ์ถ๋๋์ง ํ์ธํด๋ณด์.
// shouldTriggerEncounter(encounterRate, randomFn) ๋ด๋ถ์์
// 1
return randomFn() < 1 / encounterRate;
// generateRandomPkmon(PKMON_SPECIES, randomFn); ๋ด๋ถ์์
// 2
const randomSpecies =
commonPkmons[Math.floor(randomFn() * commonPkmons.length)];
// 3
const randomLevel = Math.floor(randomFn() * 5) + 1;
์์์๋ถํฐ ๋ณธ๋ค๋ฉด
๋จผ์ 0.05 < 1 / encounterRate === true : ์ธ์นด์ดํฐ๋ฅผ ๋ฐ์์ํค๋๋ฐ ์ฌ์ฉํ๊ณ
๊ทธ๋ฆฌ๊ณ Math.floor(0.0*commonPkmons.length) === 0 : ๋ฆฌ์คํธ ๋งจ ์์ ํจํท๋ชฌ์ ์ ํํ๊ณ
๋ง์ง๋ง์ผ๋ก Math.floor(0*5)+1 === 1 : ํจํท๋ชฌ์ ๋ ๋ฒจ์ 1๋ก ์ค์ ํ๋ค.
storage ์ถ์ํ ๊ณ์ธต ์ถ๊ฐ
// before
// utils.ts
localStorage.setItem("pkmon-storage", JSON.stringify({ state: gameData }));
// after
// utils.ts
import { storage } from "./storage";
storage.setItem("pkmon-storage", JSON.stringify({ state: gameData }));
// storage.ts
export interface StorageAdapter {
getItem(key: string): string | null;
setItem(key: string, value: string): void;
removeItem(key: string): void;
clear(): void;
}
// ํ๋ก๋์
: LocalStorageAdapter
export class LocalStorageAdapter implements StorageAdapter {...}
// ํ
์คํธ: InMemoryStorageAdapter
export class InMemoryStorageAdapter implements StorageAdapter {...}
export let storage: StorageAdapter = new LocalStorageAdapter();
์ด๋ ๊ฒ ํ๋ก๊ทธ๋จ์ด ๋ธ๋ผ์ฐ์ ์ ๋ก์ปฌ ์คํ ๋ฆฌ์ง์ ์ง์ ์ ๊ทผํ์ง ์๊ณ
localStorage / InMemoryStorage -> storageAdapter -> useGameStore ๊ฐ ๋๋ค.
์๋๋ zustand์ persist ๋ฏธ๋ค์จ์ด๋ฅผ ๋ณด๋ฉด ๊ธฐ๋ณธ์ ์ผ๋ก createJSONStorage(()=>localStorage)๋ฅผ ํตํด ๊ธฐ๋ณธ์ ์ผ๋ก localStorage์ ์ฐ๊ฒฐํ๋๋ฐ, ๋๋ storage ํ๋๋ฅผ ์
๋ ฅํ๋ค.
export const useGameStore = create<GameState>()(
persist(
(set)=>({
}),
{
name: "pkmon-storage",
version: 1,
storage: createJSONStorage(() => storage), // <-----์ฌ๊ธฐ
);
์ปค์คํ ์ด๋ํฐ๋ฅผ ๋ง๋ค์ด์ zustand๊ฐ ๊ทธ๊ฒ์ ํตํด ๋ฐ์ดํฐ๋ฅผ ์ํ๋ก ๊ด๋ฆฌํ ์ ์๊ฒ ํ๋ค.
๊ตฌํ
๊ทธ๋ ๋ค๋ฉด ์๊ฐํด๋ณด์์ผ ํ๋๊ฒ "์ด๋ํฐ๋ ๊ตฌ์ฒด์ ์ผ๋ก ์ด๋ป๊ฒ ๊ตฌํํด์ผ ํ ๊น?"
persist ๋ํ๋จผํธ๋ฅผ ๋ณด๋ฉด ์ด๋ ๋ค.
export declare const persist : Persist;
type Persist = <
T,
Mps extends [StoreMutatorIdentifier, unknown][] = [],
Mcs extends [StoreMutatorIdentifier, unknown][] = [],
U = T
>(
initializer: StateCreator<T, [...Mps, ["zustand/persist", unknown]], Mcs>,
options: PersistOptions<T, U>
) => StateCreator<T, Mps, [["zustand/persist", U], ...Mcs]>;
declare module "../vanilla.mjs" {
interface StoreMutators<S, A> {
"zustand/persist": WithPersist<S, A>;
}
}
initializer๊ณผ options ๋ ์ธ์๋ฅผ ํ์๋ก ํ๊ณ (useGameStore.tsx์ ๊ตฌํ๋์ด์์)
options์ ํ์ ์ ๋ํด์ ์์ธํ ๋ณด๋ฉด ์ด๋ ๋ค.
export interface PersistOptions<S, PersistedState = S, PersistReturn = unknown> {
name: string;
storage?: PersistStorage<PersistedState, PersistReturn> | undefined;
partialize?: (state: S) => PersistedState;
onRehydrateStorage?: (state: S) => ((state?: S, error?: unknown) => void) | void;
version?: number;
migrate?: (persistedState: unknown, version: number) => PersistedState | Promise<PersistedState>;
merge?: (persistedState: unknown, currentState: S) => S;
skipHydration?: boolean;
}
๋ด๊ฐ ์์์ผ ํ storage ์ ํ์
์ ๋ํด ๋ ์ฐพ์๊ฐ๋ณด๋ฉด ์ด๋ ๋ค.
export interface PersistStorage<S, R = unknown> {
getItem: (name: string) => StorageValue<S> | null | Promise<StorageValue<S> | null>;
setItem: (name: string, value: StorageValue<S>) => R;
removeItem: (name: string) => R;
}
์ฆ ์ด ์ธ ํจ์๋ ๋ฐ๋์ ๊ตฌํ์ ํด์ผํ๋ค๋ ๊ฒ์ ์ ์ ์๋ค.