Post

[Day51] SRP 게임 개발 연습 & useEffect

[Day51] SRP 게임 개발 연습 & useEffect

🔸 SRP(가위바위보) 게임 개발 연습

✅ 스스로 어디까지 개발 가능한가 !

개발 해야하는 UI

프로젝트 구조

어떤 순서로 개발 했는가?

  1. 프로젝트 구조 설계
    • React 프로젝트 생성
    • 폴더 구조 설정
      • /components: Card, Button 컴포넌트
      • /assets: 이미지 에셋
      • /styles: CSS or module.css
  2. 자원 준비
    • 이미지: scissors.png, rock.png, paper.png, questionmark.png
    • 스타일 파일: App.module.css 또는 App.css
  3. 컴포넌트 설계
    • App 컴포넌트: 메인 로직 및 상태 관리
    • Card 컴포넌트: 플레이어/컴퓨터의 선택과 결과를 표시
    • Button 컴포넌트: 사용자 선택 버튼 구현
  4. 일단 App.jsx에 기본 구조 하드 코딩
    • 기본 구조 안에서 App 컴포넌트 하나에 전체 로직 및 UI 작성
    • 사용자/컴퓨터 선택, 결과 출력 등 모든 기능을 App 안에서 우선 구현
  5. 게임 로직 개발
    • choice 객체: 가위/바위/보의 이름과 이미지 정보 매핑
    • 승패 판정 로직 determineWinner() 구현
    • 컴퓨터 랜덤 선택 함수generateComputerChoice() 구현
  6. 상태 관리
    • 사용자 선택(userChoice)컴퓨터 선택(computerChoice), 결과(result) 상태 정의
    • handleUserChoice 함수로 게임 흐름 제어
  7. 컴포넌트 분리
    • Card: 사용자/컴퓨터의 선택과 결과 표시
    • Button: 가위/바위/보 선택 버튼 분리
    • App은 전체 상태 및 로직을 관리하는 중심 컴포넌트로 유지
  8. UI 디테일 및 스타일 적용
    • CSS Module 사용

🐢 어느 부분에서 시간이 많이 걸렸는지?

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>
))}

import.meta.url이 뭔데?

  • ESM (ES Modules)에서만 지원되는 문법
  • Vite는 ESM 기반 번들러이기 때문에 자연스럽게 사용할 수 있음.
  • Vite에서는 정적 자산의 경로를 계산할 때 아래처럼 쓸 수 있음
1
new URL('../assets/rock.png', import.meta.url).href;

🔍 import.meta.urlVite 환경에서 자주 사용되지만, CRA(Create React App)에서는 기본적으로 사용되지 않는다.


Vite에서 이미지를 불러오는 방법

1. import.meta.url 사용

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에서 “일정 시간 후에 무언가를 실행하고 싶을 때” 사용하는 함수

기본 사용법

1
setTimeout(함수, 지연시간);
  • 함수: 일정 시간 후 실행할 코드
  • 지연시간(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. [] (빈 배열): 컴포넌트가 마운트될 때 한 번만 실행
1
2
3
useEffect(() => {
    console.log('컴포넌트가 마운트됨');
}, []);
  1. [value1, value2]: 해당 값들이 변경될 때마다 실행
1
2
3
useEffect(() => {
    console.log('count나 name이 변경됨');
}, [count, name]);
  1. 배열 생략: 모든 렌더링 후에 실행 (주의 필요)
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. 정리 함수 누락
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

This post is licensed under CC BY 4.0 by the author.