넘블챌린지의 새로운 프론트엔드 챌린지를 하게 되었다.
큰 단위는 아니지만 짧은 스프린트로 뭔가를 계속 만들어가고 거기서 고민을 하고 있다는 것에서 긍정적이라 생각한다.
이번 챌린지를 하면서도 크고 작은 고민들을 했고 그 고민들에 대해 기록하려 한다.
프로젝트의 구현 영상은 위에 gif로 첨부를 해두었다
사각형이 여러개 나오고 그중 색깔이 다른 하나를 제한시간 안에 클릭하면 다음 stage로 넘어가게 된다.
색깔은 점점 구분하기 어려워 진다. (stage 17에서 어려움을 겪는 모습을 확인할 수 있다....)
주요 코드에 대한 설명
// App.tsx
<div className="App">
<ScoreBoard stage={stage} time={time} score={score} />
<GameBoard
timeRef={timeRef}
stage={stage}
setStage={setStage}
setTime={setTime}
setScore={setScore}
/>
</div>
// ScoreBoard.tsx
export default React.memo(ScoreBoard);
// GameBoard.tsx
export default React.memo(GameBoard);
부모 컴포넌트에서 생성 후에 자식 컴포넌트에서 사용하는 변수들이 있다.
react에서 부모 컴포넌트가 rerender 되면 자식 컴포넌트 또한 rerender 되게 된다.
각각의 컴포넌트에서 활용하는 것들이 달라 react.memo를 활용하였다.
react.memo를 활용하면 props로 넘어온 친구들이 업데이트 되지 않는 이상 해당 컴포넌트는 rerender 되지 않는다.
// GameBoard.tsx
const wrongClick = () => {
setTime((time) => (time -= MINUS_TIME));
};
const goNextStage = () => {
const plusScore = getPlusScore(stage, timeRef.current);
setScore((score) => (score += plusScore));
setStage((stage) => (stage += 1));
setTime(INITIAL_TIME);
};
const blockClickHandler = (event: React.MouseEvent<HTMLDivElement>) => {
if (!(event.target instanceof HTMLDivElement)) {
return;
}
const { question } = event.target.dataset;
if (Number(question) === answer) {
goNextStage();
} else {
wrongClick();
}
};
return (
<GameBoardWrapper onClick={blockClickHandler}>
{questions &&
questions.map((question) => (
<BoardBlock
size={size}
question={question}
color={question === answer ? answerColor : baseColor}
/>
))}
</GameBoardWrapper>
);
// BoardBlock.tsx
export const BoardBlock = ({ question, color, size }: BoardBlockProps) => {
return (
<BoardBlockFragment color={color} size={size} data-question={question} />
);
};
GameBoard내에 여러개의 BoardBlock들이 stage마다 나타나게 된다.
맨 처음엔 아래 코드와 같이 boardblock에 onClick을 넣어주었다.
return (
<GameBoardWrapper onClick={test}>
{questions &&
questions.map((question) => (
<BoardBlock
size={size}
onClick={question === answer ? goNextStage : wrongClick}
color={question === answer ? answerColor : baseColor}
/>
))}
</GameBoardWrapper>
);
boardblock에서 onclick을 가지고 있는 것에서 block들을 감싸는 상위 div Element에 onClick을 달아주었다.
변경한 근거는 아래와 같다.(사실 어떤것이 더 좋고 나쁜지는 잘 모르겠다. -> 이 부분은 공부해봐야지)
1. 3항 연산자를 최대한 줄이고 싶었다.
개인적으로 코드를 읽어나갈때 분기처리가 많은것을 선호하지 않는다.
동작의 흐름을 파악하기에는 분기처리가 있는 것보다 없는것이 좋다고 생각한다. (물론 개인차에 따라 다르다고 생각한다)
2. 최근에 잘 활용한 패턴이었다.
form에서 input들을 제어해야 하는 경우가 있었다.
각각의 input에 onChange를 추가해서 다뤘었는데 form에 onChange, onFocus, onBlur를 추가해주어 하나의 함수에서 모든 input에 대한 제어를 할수 있었다.
최근의 긍정적 경험으로 말미암아 위와 같이 변경하였다(공부후에 무엇이 더 좋은지 판단이 되면 해당 포스트는 수정 예정입니다!)
단점도 있다고 생각한다.
1. 확장성이 떨어진다고 생각한다. 새로운 함수가 추가되거나 케이스가 더 생기면 handler함수 내부에서 로직이 깔끔해지지 않을것 같다.
이 부분은 현재는 정답을 모르겠다. 구글링 해서 좀 더 찾아봐야겠다(쉽지않은 코딩.....)
활용한 라이브러리와 그 이유
styled-component를 최대한 활용하려고 하였다.
최근부터 css-in-js를 활용하기 시작했고 처음엔 의구심이 있었다.
한 파일 내부에서 코드의 양이 불필요하게 길어지는건 아닐까? 하는 생각이었다.(사실 이 생각에 대한 해결책은 많다고 생각한다. 파일을 분리하면 끝이 아닌가.....)
세 개의 장점이 나에게는 크게 다가왔고 최근에는 최대한 활용을 하려 한다.
1. js 파일 내에서 css의 방식으로 코드를 작성할 수 있다.
2. 하나의 컴포넌트에 js, css가 뭉쳐있다 보니 수정이 필요할 때 큰 동작이 필요없어 수정에 수월했다.
3. styled-component에 props를 넘겨주어 상태에 따라 다른 css를 나타낼 수 있다.
1번은 react에서 inline 으로 style을 작성할때는 카멜케이스로 작성해야 하지만 css로 작성할때는 카멜케이스를 작성하지 않는다.
보통 inline css 방식으로 작성을 먼저 하고 css로 옮기는 편이였어서 여간 불편한게 아니었다.
2,3 번의 내용은 지역성의 개념에 근거한 장점인것 같다.
메모리에서 위치를 참조할때 시간에 따른 지역성과 주소에 따른 지역성이 존재한다고 생각한다.
최근에 참조한 위치는 재참조될 가능성이 높으며 주로 참조하는 위치들은 한 곳에 몰려있다.
코드도 이것과 같이 구조화 될수 있지 않을까 싶다. 한 곳을 수정하게 되면 수정한 곳에 근접한 지역에서 수정될 가능성이 높다.
파일 혹은 코드의 내용들을 근접한 위치에 두어 수정시에 불필요한 동작은 최대한 없애고 읽기 쉬운 코드를 짜고 싶은 내가 추구하고 싶은 주제다.
https://styled-components.com/
프로젝트를 진행할 때 어려웠던 점/고민했던 부분과 해결방법
- setInterval을 다루는 것이 어려웠다.
프로젝트 구현 영상을 보면 1초마다 시간이 감소한다.
이 시간이 0초가 되면 게임은 종료된다.
setInterval로 제어해야 함은 알았지만 이를 react에서 사용해본 것은 처음이었다.
처음에 component 내부에서 setInterval를 raw하게 추가해주었더니 시간이 미친듯이 감소하기 시작했다.
원인은 다음과 같다
1. 컴포넌트가 render 될때마다 setInterval 함수를 실행시켜 시간을 감소하는 함수가 중복해서 실행되고 있었다.
2. time에 의존하고 있는 컴포넌트들이 계속 업데이트 되고 업데이트 될때마다 1번을 복수로 실행하고 있었다.
해결책은 다음과 같다
useInterval customhook을 추가하고 time값을 ref로 두어 render를 하지 않게 하였다.
// src/common/hooks/useInterval.ts
import { useRef, useEffect } from "react";
export const useInterval = (callback: () => void, delay: number | null) => {
const currentCallback = useRef<ReturnType<typeof Function>>();
useEffect(() => {
currentCallback.current = callback;
}, [callback]);
useEffect(() => {
if (delay !== null) {
const id = setInterval(() => {
if (currentCallback.current) currentCallback.current();
}, 1000);
return () => clearInterval(id);
}
}, [delay]);
};
// src/App.tsx
const [isRunning, setIsRunning] = useState<boolean>(true);
const [time, setTime] = useState<number>(INITIAL_TIME);
const timeRef = useRef<number>(time);
useInterval(
() => setTime((currTime) => (currTime -= 1)),
isRunning ? 1000 : null
);
useEffect(() => {
timeRef.current = time;
if (time <= 0) {
setTime(INITIAL_TIME);
setIsRunning(false);
}
}, [time]);
return (
<div className="App">
<ScoreBoard stage={stage} time={time} score={score} />
<GameBoard
timeRef={timeRef}
stage={stage}
setStage={setStage}
setTime={setTime}
setScore={setScore}
/>
</div>
);
useInterval은 직접 고안한 hook이 아니다. 정리가 잘 되어 있는 블로그를 참고하였고 이 부분에 대해서는 나중에 상세히 포스팅 예정이다.
https://velog.io/@jakeseo_me/번역-리액트-훅스-컴포넌트에서-setInterval-사용-시의-문제점
useInterval에 대하여 간략히만 설명하면
- unmount 될때마다 cleanUp function을 통해 해당 setInterval을 clear 해주었다.
실행해보기 :
https://square-select-game-b7epetmth-woobottle.vercel.app/
'Frontend > React' 카테고리의 다른 글
제어 컴포넌트 vs 비제어 컴포넌트 (0) | 2022.03.03 |
---|---|
React twice render (0) | 2022.02.24 |
cra + typescript + jest (0) | 2022.02.23 |
cannot get /path (0) | 2022.02.07 |
cra typescript 초간단 세팅 (0) | 2022.01.17 |