공짜로 공부할 기회라고 생각하고 참가하게 되었다.
필자는 거의 TS를 사용한 경험이 없어 이에 대해 경험해보고 싶었다.
function Board({ blocks, onAnswerBlockClick, onWrongBlockClick }: BoardProps) {
return (
<div css={wrap}>
{blocks.map(({ color, isAnswer }, index) =>
isAnswer ? (
<div
key={index}
onClick={onAnswerBlockClick}
css={blockStyle(color, Math.sqrt(blocks.length))}
/>
) : (
<div
key={index}
onClick={onWrongBlockClick}
css={blockStyle(color, Math.sqrt(blocks.length))}
/>
)
)}
</div>
);
}
요즘 컴포넌트를 작성할 때 해당 컴포넌트가 얼마까지 정보(state, 처리함수 등)를 알아야 할까를 고민하며 컴포넌트를 작성하고 있다. Board
컴포넌트는 블록을 받아서 그리는 역할을 담당하고 해당 블록을 클릭했을 때 처리할 함수들만 받아서 처리하고 있다.
export const generateRandomColor = () => {
const redColor = Math.floor(Math.random() * 256);
const greenColor = Math.floor(Math.random() * 256);
const blueColor = Math.floor(Math.random() * 256);
return { redColor, greenColor, blueColor };
};
블록의 색깔은 rgb로 3가지이고 background-color: rgb($, $, $);
를 사용할 생각으로 random 함수를 이용해 랜덤 컬러를 생성해주었다.
const DIFF_COLOR_OFFSET = 2;
const DEFAULT_DIFF_COLOR_AMOUNT =
MAXIMUM_STAGE * DIFF_COLOR_OFFSET + DIFF_COLOR_OFFSET;
export const getLittleDifferentColorByStage = (
rgbColors: Colors,
stage: number
) => {
const diffAmount = DEFAULT_DIFF_COLOR_AMOUNT - DIFF_COLOR_OFFSET * stage;
const getPlusOrMinusColorValue = (color: number) => {
if (color < diffAmount) return color + diffAmount;
return color - diffAmount;
};
const { redColor, greenColor, blueColor } = rgbColors;
return {
redColor: getPlusOrMinusColorValue(redColor),
greenColor: getPlusOrMinusColorValue(greenColor),
blueColor: getPlusOrMinusColorValue(blueColor),
};
};
문제 요구사항에서 최대 스테이지 제한(MAXIMUM_STAGE
)은 없지만 스테이지가 올라감에 따라 정답 블록과 일반 블록의 컬럭값의 차이가 줄어들 수 있도록 diffAmount
를 조절했다.getPlusOrMinusColorValue
에서 컬러값을 입력받으면 diffAmount
만큼을 현재 스테이지에서 최대 스테이지까지 얼마나 차이가 있는가에 따라 컬러 색상을 다르게 주었다. if (color < diffAmount)
조건문을 준 이유는 color
값이 diffAmount
보다 작아 rgb 값이 음수가 될까봐이다.
const ONE_MICRO_SECONDS = 1000;
const useTimer = (INITIAL_TIME: number) => {
const [leftTime, setLeftTime] = useState(INITIAL_TIME);
const timerRef: { current: NodeJS.Timer | null } = useRef(null);
const onStartTimer = useCallback(() => {
if (timerRef.current !== null) return;
timerRef.current = setInterval(() => {
if (leftTime >= 0) {
setLeftTime((leftTime) => leftTime - 1);
} else {
onClearTimer();
}
}, ONE_MICRO_SECONDS);
}, []);
const onClearTimer = useCallback(() => {
if (timerRef.current === null) return;
clearInterval(timerRef.current);
console.log('clear한 후 current:', timerRef.current);
timerRef.current = null;
}, []);
const onResetTimer = useCallback(() => {
onClearTimer();
setLeftTime(INITIAL_TIME);
onStartTimer();
}, []);
const onSubtractTime = useCallback((time: number) => {
setLeftTime((leftTime) => leftTime - time);
}, []);
return { leftTime, onStartTimer, onClearTimer, onResetTimer, onSubtractTime };
};
타이머 관련해서는 먼저 넘블 챌린지를 작성하신 분이 태그한 블로그 글을 참고해서 내가 필요하다고 생각하는 부분만 커스텀 훅을 작성했다.
function App() {
...생략...
const { leftTime, onStartTimer, onClearTimer, onResetTimer, onSubtractTime } =
useTimer(MAX_TIME_LIMITED);
...생략...
useEffect(() => {
onStartTimer();
onResetScore();
return () => onClearTimer();
}, []);
}
전체적인 게임을 관리하는 App
컴포넌트에서 해당 훅을 import
해서 useEffect
함수를 활용해 최초에 한 번만 timer
가 동작하도록 하고 unmount
시 해제되도록 구현했다.
timer를 통해 매번 렌더링이 발생하기 때문에 일반적으로 블록을 그릴 경우 같은 블록이 매번 렌더링되서 비효율적이라고 생각했다.
React Dev Tool의 profiler를 활용해 어느 지점에서 계속 렌더링되는지 확인할 수 있었고, React의 React.memo
, useMemo
, useCallback
을 활용해 매번 렌더링되는 이슈를 해결했다.
위의 그림을 보면 Board
가 다시 렌더링되지 않았다는 표시를 확인할 수 있다.
이런식으로 챌린지에 참가하여 프로젝트를 구현한 적은 처음인데 동기부여도 되면서 새롭게 활용하고 공부한 것들도 많아 도움이 되었다.
기회가 된다면 다음 챌린지에도 참가를 고려해봐야 겠다.
해당 프로젝트의 코드는 여기에서 확인하실 수 있습니다.