가위바위보 게임(객체 사용하기)
컴퓨터와 가위바위보 게임하기
승리: 1점 득점/ 패배: 1점 실점
<게임 일부 설명>
사용자가 버튼을 클릭하기 전까지 화면에서 가위바위보 이미지가 일정 시간마다 바뀌는 기능에 사용할 수 있다.
버튼이 클릭되면 이미지가 멈추면서 사용자가 선택한 것과 비교해서 승부를 가른다.
상대 경로(점 있음)
내 위치를 기준으로 파일을 가리킨다(시작점 변경될 수 있음)
./ : 현재 파일이 위치한 폴더(현재 폴더)
../ : 내가 위치한 폴더의 상위 폴더(부모 폴더)
절대 경로(점 없음)
C드라이브를 기준의 절대적인 위치로 파일을 가리킨다.(항상 시작점 동일)
/ : 루트 폴더(ex. Window의 경우 C드라이브) - 서버 개발할 때 사용됨
: 서버에 이미지를 요청하는 횟수를 줄이기 위해 여러 개의 이미지를 하나의 이미지로 합쳐서 관리하는 이미지
웹페이지에 이미지가 사용될 경우 이미지를 다운받기 위해 웹 브라우저는 서버에 이미지를 요청하는데 요청하는 이미지가 많을수록 웹페이지의 로딩 시간이 길어진다.
웹 페이지의 로딩 시간을 단축하기 위해 이미지 스프라이트를 사용해 서버 요청을 줄일 수 있다.
▶ 변수.style.background = 'url(${이미지 경로}) X좌표 Y좌표';
background 속성값으로 이미지 경로와 background-position을 작성한다.
(X좌표가 음수이면 왼쪽으로 이동, Y좌표가 음수이면 위로 이동)
하나의 이미지에서 가위바위보 중 해당 이미지를 화면에 나타내기 위해 X좌표를 이동한다.
가위바위보 각각의 X좌표는 타이머가 돌면서 특정 시간마다 화면에 표시되는 이미지를 바꿔줘야 하므로 X좌표를 각각의 변수에 저장한다.
'X좌표'라는 공통점이 있어 객체로 묶어서 표현하는 것이 낫다.
const rspX = {
scissors: '0',
rock: '-220px',
paper: '-440px',
};
여러 개의 변수를 하나의 객체로 묶으려면(그룹화) 공통점이 있어야 한다.
(동일한 이름으로 하나의 변수에 여러개의 값을 넣을 때 사용)
최대한 그룹화하여 객체로 묶음으로써, 사용했던 변수를 또 사용해 변수명이 겹쳐 발생하는 에러를 막을 수 있다.
객체의 변수명은 재사용하지 않고, 고유해야 한다.
▶ 변수.style.backgroundSize = 'auto 200px';
큰 이미지를 화면 사이즈에 맞게 줄이기 위해 이미지 크기를 조절하는 속성이다.
'가로 세로의 비율을 유지하면서 세로 길이가 200px이 될 때까지 이미지를 줄인다.'는 의미이다.
backgroundSize
속성은 background
속성과 항상 같이 나온다! 짝꿍💑
(background 속성만 변경하더라도 backgroudnSize 속성을 다시 적어줘야 한다! backgroudnSize를 생략하면 리셋 된다.)
setInterval로 타이머 돌면서 '가위✌->바위✊->보🖐' 순서대로 이미지 변경을 반복하기
if - else if - else문을 사용할 때, 마지막 else에서 코드 상 문제가 되지 않지만, else라고 생각하는 조건 외의 경우에 해당 코드가 실행될 수 있으므로 'else if'로 조건을 명확하게 작성해주는 것이 좋다.
※ 초보자들이 헷갈려하는 점! 유의점①👩🏫
객체에서 변수의 값을 적는 경우, 대괄호를 사용해 배열처럼 코드를 작성해야 한다!
setInterval(함수, 타이머)
: 지정한 시간마다 주기적으로 설정한 함수를 반복적으로 실행 (setTimeout은 한 번만 실행)
첫번째 인자는 함수 자리로 괄호'()'를 작성하면 리턴값이 들어가 undefined이 되기 때문에 괄호 없이 함수명만 작성해야 한다.
재귀함수 : 하나의 함수 안에서 자신을 다시 호출하는 함수
실행 코드를 함수로 따로 만들어 빼놓고, setInterval() 함수 대신 setTimeout() 함수를 재귀함수 형태로 함수 안에 자기를 다시 호출함으로써 계속 실행할 수 있다.
// setInterval() 함수
let computerChoice = 'scissors';
const changeComputerHand = () => {
if (computerChoice === 'scissors') {
computerChoice = 'rock';
} else if (computerChoice === 'rock') {
computerChoice = 'paper';
} else if (computerChoice === 'paper') {
computerChoice = 'scissors';
}
$computer.style.background = `url(${IMG_URL}) ${rspX[computerChoice]} 0`;
$computer.style.backgroundSize = 'auto 200px';
}
setInterval(changeComputerHand, 500);
// 재귀 setTimeout() 함수로 대체(재귀함수 이용)
let computerChoice = 'scissors';
const changeComputerHand = () => {
if (computerChoice === 'scissors') {
computerChoice = 'rock';
} else if (computerChoice === 'rock') {
computerChoice = 'paper';
} else if (computerChoice === 'paper') {
computerChoice = 'scissors';
}
$computer.style.background = `url(${IMG_URL}) ${rspX[computerChoice]} 0`;
$computer.style.backgroundSize = 'auto 200px';
setTimeout(changeComputerHand, 500); // 함수 안에서 자기 자신을 호출
}
setTimeout(changeComputerHand, 500);
JavaScript 작동 원리 상 setInterval과 setTimout은 다르다.
setInterval() 함수는 타이머 간격을 보장하기 위해 노력하지만
setTimeout() 함수는 앞의 코드 실행시간이 오래걸려도 코드 실행이 끝나야 타이머가 반응하기 때문에 대기시간이 생길 수 있다. 타이머로 설정한 간격을 보장하지 못해 설정한 시간 이상으로 걸릴 수 있다.
setInterval 함수는 clearInterval 함수로 취소할 수 있다.
setTimeout 함수는 clearTimout 함수로 취소할 수 있다.
clear~ 함수로 취소하기 위해서 set~ 함수가 반환하는 아이디가 필요하다.
let id = setInterval(함수, 밀리초);
clearInterval(id);
let id = setTimeout(함수, 밀리초);
clearTimeout(id);
// setTimeout에 지정한 함수가 실행되기 전에 clearTimeout 함수로 취소할 수 있음
setInterval 함수는 반환값을 가지는데 타이머에 대한 아이디(숫자)를 리턴한다. 아이디를 이용해 clearTimeout(아이디) 함수를 호출해야 한다.
타이머를 멈추기 위해 타이머 함수의 인수로 넣은 '함수가 실행되기 전에 clearTimeout(아이디) 함수를 호출'해야 한다.
이전 타이머로 현재 타이머를 취소할 수 없다.
인수로 쓰이는 id는 타이머가 생성될 때마다 id가 계속 달라지기 때문에 항상 id 값을 변수에 저장해두어야 매번 clearInterval(intervalId)이 이루어질 수 있다. (인자로 들어가는 intervalId가 현재 타이머의 id여야하기 때문!)
let intervalId = setInterval(changeComputerHand, 200); // id가 리턴값으로 저장
const clickButton = () => {
clearInterval(intervalId); // 현재 타이머의 id로 interval 함수 취소
setTimeout(() => { // 1초 쉬었다가 다시 게임 스타트
intervalId = setInterval(changeComputerHand, 200); // 또다시 clear하기 위한 현재 타이머의 id를 변수에 저장
}, 1000);
};
$scissors.addEventListener('click', clickButton);
Bug🐛
1. 한 번에 연속 (5번)클릭하면 이미지 돌아가는 속도가 빨라짐
버튼을 클릭하면 clearInterval만을 수행해 마지막으로 생성된 intervalId만 멈추게 되고, setTimeout을 멈추는 clearTimeout은 수행하지 않는다.
clearInterval은 Interval만을 멈추게 하고, 계속 생기는 setTimeout을 취소하지 못해 여러 번 호출되는 문제가 발생되는 것이다.
결국 setTimeout으로 실행된 setInterval은 버튼을 누른 횟수만큼 호출되고 설정한 타이머 시간동안 호출된 갯수만큼 실행해 빠른 속도로 돌아간다.
(-> 2번 부가 설명으로 넘어가기)
Bugless🐛💀
※ 비동기 코드일 때 유의점!
비동기 코드인 setTimeout 함수가 실행될 때, '설정한 타이머 동안 직전 intervalId가 있으면 먼저 제거한 후' 자기 함수를 호출해 다시 실행한다.
클릭하고 타이머가 실행되는 1초 동안에 혹시 모를 실행되는 코드가 있을 수 있기 때문에 삭제하는 작업을 해주는 것이다.
🔽 클릭할 때 바로 실행되는 intervalId를 제거하는 코드와 동일한 코드이지만 실행되는 시점이 다르기 때문에 중복 코드가 아니다!
let intervalId = setInterval(changeComputerHand, 200);
const clickButton = () => {
clearInterval(intervalId); // click하자마자 실행
setTimeout(() => {
clearInterval(intervalId); // 1초 뒤에 실행
intervalId = setInterval(changeComputerHand, 200);
}; 1000);
};
$rock.addEventListener('click', clickButton);
addEventListener로 연결한 함수를 removeEventListener로 제거할 수 있다.
(이벤트 연결한 함수와 제거할 함수가 같아야 한다! 함수는 참조 관계여야 한다!)
타이머가 실행되는 1초 동안 버튼을 클릭해도 클릭 함수의 내부 함수가 실행되지 못하게 할 수 있다.
function 함수() {}
변수.addEventListener('event', 함수);
변수.removeEventListener('event', 함수);
타이머가 실행되는 동안 클릭했을 때 버튼이 동작하지 못하도록 클릭 이벤트를 제거한다. 설정한 타이머 이후에는 다시 정상 작동하도록 원래 작성했던 이벤트가 실행된다.
const clickButton = () {
clearInterval(intervalId);
$scissors.removeEventListener('click', clickButton); // 버튼 클릭해도 클릭 이벤트 삭제
setTimeout(() => {
intervalId = setInterval(changeComputerHand, 200);
$scissors.addEventListener('click' clickButton); // 다시 이벤트 추가
}, 1000);
};
$scissors.addEventListener('click', clickButton);
이벤트 추가/삭제를 반복하면 문제가 발생될 수 있으므로 true/false에 따라서 코드 실행여부를 결정하는 '플래그 변수' 이용하는 것을 추천한다. (removeEventListener를 제거/추가를 반복하면 실수할 가능성 있음)
let clickable = true; // 플래그 변수 기본값 true로 설정
const clickButton = () => {
if (clickable) {
clearInterval(intervalId); // 타이머 취소되면
clickable = false; // 이벤트 실행되지 못하게 플래그 변수 변경
setTimeout(() => {
clickable = true;
intervalId = setInterval(changeComputerHand, 200);
}, 1000);
}
};
※ 초보자들이 헷갈려하는 점! 유의점②👩🏫
addEventListener 이후 removeEventListener가 적용되지 않아 이벤트가 삭제되지 않는다?!
add 이벤트 함수와 remove 이벤트 함수 인자에 들어가는 값이 동일해야 remove가 적용되는데 다른 값이기 때문에 addEventListener의 이벤트가 제거되지 않는다.(이러한 이유로 플래그 변수를 추천)
함수는 객체의 한 종류이다. 인자에 들어간 함수명이 동일하다고 같은 값이 아니다.
=> 참조 관계를 유지하고 싶다면 변수에 담아 그 변수를 계속 재사용한다.
// 예시
const fun = (값) => () => {
console.log('고차함수', 값);
}
const fun1 = fun(1); // 변수에 저장
ele.addEventListener('click', fun1);
ele.removeEventListener('click', fun1);
고차함수인 fun은 항상 새로운 함수를 반환한다. addEventListener와 removeEventListener에 각각 넣은 함수는 같은 함수가 아니므로 이벤트가 제거되지 않는다.
이벤트를 제거하려면 fun(1)을 fun1 변수에 저장해서 같은 함수라는 것을 보장해야 한다.
내가 클릭해 선택한 요소를 event.target을 이용해 찾아 컴퓨터가 선택한 것과 비교하기
if - else문을 이용해 9가지 경우의 수를 작성할 수 있으나 코드 중복이 많다.
중복을 제거하기 위해 각각 변수에 특정 수를 대입하여 사칙연산 결과를 규칙표로 작성하여 규칙을 찾는다.
'내 점수 - 컴퓨터 점수'(뺄셈)의 결과를 이길 때와 질 때의 경우로 나눠 조건문을 작성할 수 있다.
변수에 수를 대입하는 것을 객체 형태로 만들어 변수명을 사용할 수 있다.
(알고리즘 문제에서 문자열이 많이 나오는 경우, 문자열->'숫자'로 바꿔보면 규칙이 보일 때가 있다.)
✨ 0cho님의 🍯Tip!!
조건문 작성 시, 조건의 경우가 많아 '||(or)'을 많이 쓴다면 배열로 만들고 includes() 함수로 줄일 수 있다.
ex. color === 'red' || color === 'blue' || color === 'green'
['red', 'blue', 'green'].includes(color) (<-배열 안에 값들 중 color가 들어있는가를 의미) 또는
['red', 'blue', 'green'].indexOf(color) > -1 로 가능하다.
includes() 함수와 indexOf() 함수는 true/false로 확인할 수 있다.
사람이 읽기 쉽게 코드 작성하는 방법 익히기!
코드를 무작정 줄인다고 좋은 것은 아니며 코드를 줄이면 주석으로 부연 설명을 작성해야할 수 있다.
※ 실제 프로그램을 만들 때 항상 예외가 발생😵할 수 있음을 머릿속에 염두해 두기!
① if - else문에서 마지막 조건을 else 대신 'else if'로 조건을 명확하게 작성
②논리적으로 특정 경우만 조건에 해당하므로 조건문에 '==='를 사용하기보다 예외인 경우를 생각해서 '>='를 작성해 조건을 넓히거나 줄이기