유저가 입력한 1 이상 99 이하의 정수 를 컴퓨터가 맞춰내는 게임을 만들고 있다.
- 유저가 입력한 정수를 제외하고 (즉, 프로그램은 첫번째 시도에서 정답을 맞출 수는 없다)
- 랜덤 정수를 생성한다.
- 이에 따라 유저가 Up or Down 을 입력하면
- 프로그램은 그 랜덤 정수를 최대값 or 최소값 으로 업데이트하여
- 새로운 랜덤 정수를 생성한다.
- 이를 정답을 맞출 때까지 반복한다.
(임포트 문은 생략했다)
// min, max : 최소값, 최대값
// exclude: 제외할 정수 (최초 입력값 & 이미 생성한 랜덤값)
const generateNumBetween = ( min: number, max: number, exclude: number): number => {
const randomNum = Math.floor(Math.random() * (max - min)) + min; // 랜덤 정수 생성
//컴퓨터는 첫 시도에서 답을 맞출 수 없음.
if (randomNum === exclude) {
//생성된 숫자가 제외돼야하는 정수와 같다면 다시 생성
return generateNumBetween(min, max, exclude);
} else {
return randomNum;
}
};
매개변수 min, max, exclude는 숫자형이고, 이들을 통해 실행된 함수 generateNumBetween은 숫자형을 반환함을 명시한다.
랜덤 정수를 생성한 방법을 보다 자세히 설명해보자면 아래와 같다.
const randomNum = Math.floor(Math.random() * (max - min)) + min
max - min // 범위 크기 설정 (예: 1부터 100이라면 범위는 99)
Math.random() // 0부터 1사이의 난수 생성
Math.random() * (max - min) // 범위 내의 난수 생성
Math.floor() // 소수점 아래 자리 반올림
Math.floor(Math.random() * (max - min)) // 생성한 랜덤 난수의 소수점 아래 자리 반올림
+ min // min 기준으로 원하는 범위로 이동 (현재는 100 - 1 = 99 -> 0부터 99 이므로, 1부터 100으로 교정)
Math.floor(Math.random() * (max - min)) + min // 완성
이제 게임 화면을 구성한다.
// 타입 선언
interface GameScreenProps {
userNumber: number;
onGameOver: () => void;
}
// 최소값, 최대값 변수 설정
let minBoundary = 1;
let maxBoundary = 100;
export default function GameScreen({ userNumber, onGameOver }: GameScreenProps) {
// 첫번째로 추측할 랜덤 정수
const initialGuess = generateNumBetween(minBoundary, maxBoundary, userNumber);
const [currentGuess, setCurrentGuess] = useState(initialGuess);
useEffect(() => {
if (currentGuess === userNumber) {
onGameOver() // 정답을 맞춘다면 게임오버 화면으로
}
}, [currentGuess, userNumber, onGameOver]);
//유저가 +,-를 올바른 방향으로 사용하도록 유도
const nextGuessHandler = (direction: string) => {
if (
(direction === "lower" && currentGuess < userNumber) ||
(direction === "greater" && currentGuess > userNumber)
) {
Alert.alert("정말인가요?", "다시 한 번 생각해보세요.", [
{ text: "아차차", style: "cancel" },
]);
return; // return을 쓰지 않으면 alert창이 뜨는 동시에 새로운 정수가 생성된다.
}
//수를 올릴 방향. 키울지 줄일지?
if (direction === "lower") {
maxBoundary = currentGuess;
} else {
minBoundary = currentGuess + 1;
}
console.log(minBoundary, maxBoundary); //범위값이 계속 업데이트 되는 것을 확인할 수 있다
// 새로운 랜덤 정수 생성
const newRandomNum = generateNumBetween( minBoundary, maxBoundary, currentGuess );
setCurrentGuess(newRandomNum); //업데이트된 최소값, 최대값, 최근에 썼던 정수를 배제
};
useEffect문의 의존성 배열에 currentGuess, userNumber, onGameOver 가 들어가는 이유?
currentGuess
- currentGuess와 userNumber가 일치하는지 여부를 확인하여 게임을 종료해야 한다.
- 따라서 currentGuess가 바뀔 때마다 if (currentGuess === userNumber) 를 검사한다.
userNumber
- userNumber가 바뀐다는 것은 새로운 게임이 시작되었다는 것을 의미하므로, (유저가 게임 도중 바꾸는 경우 포함( 이 로직을 다시 실행해야한다
onGameOver
- onGameOver 는 정답 맞췄을 때 실행되는 콜백함수이다.
- 따라서 onGameOver의 함수 구현이 변경되었다면 useEffect문을 수행해야 한다.
마지막으로 리턴문을 작성한다.
return (
<View>
<NumberContainer>{currentGuess}</NumberContainer>
<View>
<Text>Higher or Lower?</Text>
<View>
<Button onPress={nextGuessHandler.bind(this, "lower")}>
-
</Button>
<Button onPress={nextGuessHandler.bind(this, "greater")}>
+
</Button>
</View>
</View>
</View>
);
}
그런데 여기서 프로그램이 정답을 맞춘 순간 Render Error: Maximun call stack size exceeded 가 반환된다.
이 문제는 렌더링 순서와 관련이 있다.
기본적으로 React의 렌더링 순서는 아래와 같다.
- 컴포넌트의 모든 함수를 실행
- useState 초기화
- useEffect 실행
이 경우에 대입하여 설명하자면, 렌더링은 아래와 같은 순서로 실행된다.
- generateNumBetween 함수를 실행하여 initialGuess 생성
- initialGuess 값을 받아 상태를 초기화
- initialGuess 와 currentGuess 에 초기값을 설정한 후 useEffect 함수 실행
결론
- useEffect문은 currentGuess가 변경될 때마다 실행되어 게임 종료 조건을 확인하는 것이 최종 목적인데-
- 정답을 고른 순간, nextGuessHandler가 실행되고 setCurrentGuess를 호출하여 상태를 업데이트하고 리렌더링을 준비한다.
- 그러나 정답은 minBoundary와 maxBoundary가 같은 경우이므로, 유효한 범위가 없기 때문에 nextGuessHandler는 새로운 랜덤 정수를 생성할 수 없다.
- 이 때 generateNumBetween는 유효한 범위를 찾을 때까지 재귀적으로 계속 호출되게 된다.
- 따라서 useEffect문보다 먼저 실행되는 generateNumBetween함수가 무한 루프에 걸려 useEffect문은 실행되지 못하고, 이에 따라 재렌더링이 미처 이루어지지 못한다.
해결 방법
절대적으로 안전한 방법일 것 같지는 않아, 추후 더 고민해보기
//이전 코드
const initialGuess = generateNumBetween(minBoundary, maxBoundary, userNumber);
//바꾼 코드
const initialGuess = generateNumBetween(1, 100, userNumber);
위처럼 초기 최소값, 최대값을 고정하여 하드코딩 하는 방법이 있다.
이렇게 하면 generateNumBetween은 항상 유효한 범위를 가질 수 있다.
따라서 유효한 범위는 지키는 선에서 에러를 반환하던 함수를 해결하고, useEffect문을 수행해 화면 전환을 이룰 수 있다.