🔸 SRP(가위바위보) 게임 개발 연습
✅ 스스로 어디까지 개발 가능한가 !
개발 해야하는 UI
프로젝트 구조
어떤 순서로 개발 했는가?
- 프로젝트 구조 설계
- React 프로젝트 생성
- 폴더 구조 설정
/components
: Card, Button 컴포넌트/assets
: 이미지 에셋/styles
: CSS or module.css
- 자원 준비
- 이미지: scissors.png, rock.png, paper.png, questionmark.png
- 스타일 파일: App.module.css 또는 App.css
- 컴포넌트 설계
- App 컴포넌트: 메인 로직 및 상태 관리
- Card 컴포넌트: 플레이어/컴퓨터의 선택과 결과를 표시
- Button 컴포넌트: 사용자 선택 버튼 구현
- 일단
App.jsx
에 기본 구조 하드 코딩- 기본 구조 안에서 App 컴포넌트 하나에 전체 로직 및 UI 작성
- 사용자/컴퓨터 선택, 결과 출력 등 모든 기능을 App 안에서 우선 구현
- 게임 로직 개발
- choice 객체: 가위/바위/보의 이름과 이미지 정보 매핑
- 승패 판정 로직
determineWinner()
구현 - 컴퓨터 랜덤 선택 함수
generateComputerChoice()
구현
- 상태 관리
- 사용자 선택(userChoice)컴퓨터 선택(computerChoice), 결과(result) 상태 정의
handleUserChoice
함수로 게임 흐름 제어
- 컴포넌트 분리
Card
: 사용자/컴퓨터의 선택과 결과 표시Button
: 가위/바위/보 선택 버튼 분리App
은 전체 상태 및 로직을 관리하는 중심 컴포넌트로 유지
- UI 디테일 및 스타일 적용
🐢 어느 부분에서 시간이 많이 걸렸는지?
1. 비동기 상태 업데이트로 인한 승패 판정 오류
❌ 문제 상황
- 처음에는 userChoice, computerChoice를 setState 한 뒤, 바로 determineWinner 함수로 승패 판정을 시도했음.
- 하지만 setState는 비동기 -> 즉시 실행되는 로직은 이전 상태를 참조함
- 그 결과, 항상 이전 게임 결과가 출력되는 오류 발생.
1
2
3
4
5
6
7
8
| // 기존 코드
const handleUserChoice = (userSelector) => {
setUserChoice(userSelector);
generateComputerChoice();
const gameResult = determineWinner(userChoice, computerChoice);
setResult(gameResult); // 이전 상태 기준으로 판정됨
}
|
✅ 해결 방법
- 상태 업데이트 전에 필요한 값을 먼저 변수로 저장하고,
- 그 값을 기반으로 승패 판정을 수행한 뒤, 최종적으로 상태 업데이트.
1
2
3
4
5
6
7
8
9
| // 수정된 코드 : 먼저 값을 계산한 뒤 상태 업데이트
const handleUserChoice = (userSelector) => {
const computerSelect = generateComputerChoice();
const gameResult = determineWinner(userSelector, computerSelect);
etResult(gameResult);
setComputerChoice(computerSelect);
setUserChoice(userSelector);
};
|
2. 이미지 경로 문제
❌ 문제 상황
- value 값으로 scissors, rock, paper를 사용해서 이미지 파일명과 매핑하려 했지만, value가 동적이라 경로 계산이 헷갈림.
- 특히 import.meta.url을 활용한 상대 경로 설정이 익숙하지 않아서 시간 소요됨.
1
2
3
4
5
| const choices = [
{ name: "가위", value: "scissors" },
{ name: "바위", value: "rock" },
{ name: "보", value: "paper" },
];
|
1
2
| // 기존 코드 : 이렇게는 안 됨 (Vite에서는 import되지 않은 assets는 빌드 시 무시됨)
<img src = {`../assets/${choice.value}.png`} />
|
✅ 해결 방법 : import.meta.url
+ new URL
- new URL() 방식으로 상대 경로 문제 해결
- 반복문 내에서 value를 활용해 이미지 파일 경로를 동적으로 지정
1
2
3
4
| <img
src={new URL(`../assets/${choice.value}.png`, import.meta.url).href}
alt={choice.name}
/>
|
적용 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
| {choices.map((choice) => (
<button
key={choice.value}
className={choice.value}
onClick={() => onClick(choice.name)}
>
<img
src={new URL(`../assets/${choice.value}.png`, import.meta.url).href}
alt={choice.name}
/>
<p>{choice.name}</p>
</button>
))}
|
- ESM (ES Modules)에서만 지원되는 문법
- Vite는 ESM 기반 번들러이기 때문에 자연스럽게 사용할 수 있음.
- Vite에서는 정적 자산의 경로를 계산할 때 아래처럼 쓸 수 있음
1
| new URL('../assets/rock.png', import.meta.url).href;
|
🔍 import.meta.url
은 Vite 환경에서 자주 사용되지만, CRA(Create React App)에서는 기본적으로 사용되지 않는다.
Vite에서 이미지를 불러오는 방법
1
2
| const imgUrl = new URL('../assets/rock.png', import.meta.url).href;
<img src={imgUrl} alt="바위" />;
|
📌 특징
- Vite 전용 방식
- import.meta.url은 현재 파일의 URL을 의미함
- new URL()을 통해 상대 경로를 절대 경로로 변환해줌
- 이미지, JSON, 기타 자산을 안전하게 불러올 수 있음
- 동적 경로 처리할 때 유용 (map 돌릴 때 등)
2. 아예 import 시켜버리기
1
2
| import rockImg from '../assets/rock.png';
<img src={rockImg} alt="바위" />
|
📌 특징
- 가장 직관적이고 간단한 방식
- Vite, CRA 모두 지원
- 이미지가 모듈처럼 처리돼서 경로 오류 없이 안전함
- 단점: 동적으로 파일명을 바꿀 수 없음 (ex: ../assets/${value}.png 불가)
3. public 폴더 + 절대 경로
1
| <img src="/rock.png" alt="바위" />;
|
📌 특징
- public/ 폴더에 있는 정적 파일 접근 방식
- vite.config.js 없이도 / 경로 기준으로 바로 접근 가능
- Vite나 CRA 모두 호환
- 번들링 대상이 아니라서 파일명 해시 처리나 최적화가 안 됨
- 동적으로도 쉽게 처리 가능
🚫 이런 건 놉 !
1
2
| <img src={`../assets/rock.png`} /> // Vite에서 경로 못 찾음
<img src="../assets/rock.png" /> // Vite에서 번들링 과정에 포함 x -> 빌드 시 이미지 깨짐
|
👨🏫 강사님 방식과 비교해서 배운 점
1. 조건부 스타일링
- Card 컴포넌트에 직접 className 조합
- 결과(승/패/무)에 따라 스타일 클래스 결정
1
2
3
4
5
| // 내가 고민하던 복잡한 조건
<article className={`${css.card} ${result === '승' ? css.win : result === '패' ? css.lose : css.draw}`}/>
// 강사님 코드: 함수 하나로 처리
<article className={`${css.card} ${getResultClass()} ${css[type]}`}/>
|
⭐️ 고민했던 방법은 삼항 연산자를 써서 조건을 처리였지만, 이렇게 클래스를 함수로 분리해서 관리하니 더 읽기 쉽고 명확했다.
2. 더 깔끔하게 이미지 매핑
- 선택한 값(가위, 바위, 보)을 이미지와 스타일명으로 매핑하는 객체 사용
- 보기 좋고 유지보수도 쉬움
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| // 이미지 import
import rockImg from "../assets/rock.png";
import paperImg from "../assets/paper.png";
import scissorsImg from "../assets/scissors.png";
const Button = ({ choice, onClick, disabled }) => {
// 선택 -> 이미지 및 스타일 매핑
const choiceMap = {
가위: { image: scissorsImg, style: "scissors" },
바위: { image: rockImg, style: "rock" },
보: { image: paperImg, style: "paper" },
};
const { image, style } = choiceMap[choice];
return (
<button
className={`${css.button} ${css[style]}`} // 동적으로 클래스 이름을 선택
disabled={disabled}
onClick={onClick}
>
<img src={image} alt={choice} className={css.buttonImage} />
<span className={css.buttonText}>{choice}</span>
</button>
);
};
export default Button;
|
⭐️ 버튼마다 “가위”, “바위”, “보”처럼 바뀌는 상황에서는 css[style]처럼 변수 기반 접근이 필요함
1
2
| css.scissors // 정적 접근 (직접 이름을 써야 함)
css[style] // 동적 접근 (변수로 접근 가능)
|
3. setTimeout() 전역 함수
JavaScript에서 “일정 시간 후에 무언가를 실행하고 싶을 때” 사용하는 함수
기본 사용법
- 함수: 일정 시간 후 실행할 코드
- 지연시간(ms): 밀리초 단위 (1000ms = 1초)
SRP에 적용
1
2
3
4
5
6
| setTimeout(() => {
const compChioce = generateComputerChoice();
setComputerChoice(compChioce);
setResult(determineWinner(choice, compChioce));
setIsPlaying(false);
}, 300);
|
- 사용자 선택 직후에 바로 결과가 보이지 않게 하고,
- 약간의 지연을 줘서 자연스러운 애니메이션/효과 연출 가능
- UX 향상을 위한 의도적인 딜레이 처리
clearTimeout()
1
2
3
4
5
| const timeoutId = setTimeout(() => {
console.log("취소 안 됨");
}, 5000);
clearTimeout(timeoutId); // 실행 안 됨
|
- setTimeout() 안에 있는 코드는 비동기적으로 실행됨. → 메인 흐름이 끝난 뒤, 대기시간이 지난 후 실행됨.
- 컴포넌트 안에서 사용할 땐 컴포넌트가 언마운트되기 전에 clearTimeout()을 해줘야 메모리 누수 방지 가능.
자주 쓰이는 패턴
1
2
3
4
5
6
7
8
9
10
| // 어떤 요소를 1초 뒤에 보여주고 싶은 상황
useEffect(() => {
const timeoutId = setTimeout(() => {
setShowResult(true);
}, 1000);
return () => {
clearTimeout(timeoutId); // 언마운트 시 정리
};
}, []);
|
실습 결과
2시간 실습 결과물
최종 구현 UI
🔸 useEffect
useEffect
는 컴포넌트의 렌더링 외에 발생하는 ‘부수 효과(side effects)’를 처리하기 위한 도구
부수 효과란? “화면에 보여주는 것” 외에 필요한 모든 작업
- 데이터 가져오기 (API 호출) - fetch, axios
- 타이머 설정하기 (setTimeout)
- 이벤트 리스너 등록
- 외부 시스템과 연동
기본 구조
1
2
3
4
5
6
| useEffect(() => {
// 마운트될 때 실행할 코드
return () => {
// 언마운드 될 때 실행할 코드 - 정리(cleanup) 함수 (선택적)
};
}, [의존성1, 의존성2, ...]); //의존성이 업데이트 되면 useEffect() 재 실행
|
- 의존성 배열에 명시한 값이 바뀔 때마다 이 함수가 다시 실행
- 정리 함수는 선택적이며, 주로 메모리 누수를 방지하기 위해 사용
useEffect 실행 시점
실행 시점 | 설명 |
---|
마운트 시 | 컴포넌트가 처음 나타날 때 |
업데이트 시 | 의존성 배열에 있는 값이 변경될 때 |
언마운트 시 | 컴포넌트가 사라질 때 (정리 함수 실행) |
의존성 배열
의존성 배열 | 설명 |
---|
빈 배열 [] | 마운트 시 한 번만 실행 |
[value1, ...] | 의존성 값이 변경될 때 실행 |
생략 | 모든 렌더링 후 실행 (비권장) |
[]
(빈 배열): 컴포넌트가 마운트될 때 한 번만 실행
1
2
3
| useEffect(() => {
console.log('컴포넌트가 마운트됨');
}, []);
|
[value1, value2]
: 해당 값들이 변경될 때마다 실행
1
2
3
| useEffect(() => {
console.log('count나 name이 변경됨');
}, [count, name]);
|
- 배열 생략: 모든 렌더링 후에 실행 (주의 필요)
1
2
3
| useEffect(() => {
console.log('컴포넌트가 렌더링됨');
}); // 의존성 배열 없음!
|
정리(Cleanup) 함수
- 이전 효과를 정리하기 위한 함수
- 메모리 누수 방지에 중요
- 이벤트 리스너, 타이머, 구독 등을 해제할 때 필요
🚨 흔한 실수
1. 무한 루프 발생
1
2
3
| useEffect(() => {
setCount(count + 1); // Effect가 상태를 변경하면 다시 렌더링됨
}, [count]); // count가 변경되면 Effect 실행 -> 무한 루프!
|
2. 의존성 배열 누락
1
2
3
| useEffect(() => {
console.log(user.name); // user.name 사용
}, []); // user.name을 의존성으로 추가해야 함
|
- 정리 함수 누락
1
2
3
4
5
6
| useEffect(() => {
const timer = setInterval(() => {
// 작업 수행
}, 1000);
// cleanup 함수가 없어서 메모리 누수 발생!
}, []);
|
✅ 정리 함수 포함
1
2
3
4
5
6
| useEffect(() => {
const timer = setInterval(() => {
// 작업 수행
}, 1000);
return () => clearInterval(timer);
}, []);
|
요약
useEffect
는 렌더링 외의 “부수 효과”를 처리하는 Hook- 의존성 배열을 통해 실행 시점 제어 가능
- 정리(cleanup) 함수로 메모리 누수 방지
- 외부 시스템 연결, 데이터 페칭, DOM 조작 등에 활용
🔸 의문점
Q.
useEffect에서 배열이 생략되었을 때, 모든 렌더링 후에 실행된다는게, 마운트가 되고 렌더링이 다 된 후에 실행 된다는 의미일까요?
✅ A.
1
2
3
4
5
6
7
8
9
10
| useEffect의 의존성 배열이 생략되었을 때의 동작에 대해 좀더 풀어서 설명하면~
useEffect에서 두 번째 인자인 의존성 배열을 완전히 생략하면(즉, useEffect(() => {...}) 형태로 사용하면),
컴포넌트가 처음 마운트될 때 한 번 실행
그 이후에는 컴포넌트가 리렌더링될 때마다 매번 실행
즉, 렌더링이 완료된 후에 이펙트가 실행된다는 점은 맞습니다.
React는 먼저 화면을 업데이트한 다음에 useEffect 콜백을 실행합니다.
|
타입 | 실행 시점 |
---|
빈배열 [] | 처음만 |
배열 생략 | 매번 (마운트 + 리렌더링) |
강사님 답변을 간략히 표로 정리했다 !
참고하기 좋은 UI 관련 라이브러리
- https://swiperjs.com/
- https://gsap.com/
- https://michalsnik.github.io/aos/
- https://tympanus.net/codrops/
END