안녕하세요.
오늘은 카드 짝맞추기 게임을 하면서 배웠던 주요 내용들을 다뤄보며 복습하는 시간을 가지려고 합니다.
바로 시작하겠습니다.
1. classList.toggle('ClassName');
토글은, classList.add() 와 classList.remove() 두 함수를 묶은 것입니다.
즉, 클릭하면 클래스를 생성해주고, 다시 클릭하면 클래스를 삭제해주는 역할까지 합니다.
잘 이해가 안되시면 동영상을 참조해주세요.
코드를 한번 살펴보겠습니다.
let text = document.querySelector('div');
let btn = document.querySelector('.btn');
btn.addEventListener('click', function(){
text.classList.toggle('back');
})
이런식으로 버튼에 이벤트리스너를 달고, div태그를 text라는 변수에 담아준뒤에,
text.classList.toggle('추가/삭제할 클래스') 이라는 명령어를 통해서 토글을 사용할 수 있습니다.
그럼 미리 정해진 'back'이라는 클래스의 css요소가 적용되어 저런식으로 글자 색을 변화시키는 등의 효과를 줄 수 있습니다.
2. includes
includes는 배열 내, 원하는 찾는 값이 있는지 유무를 검사해 있으면 true, 없으면 false를 반환하는 함수입니다.
let ary = [1,2,3,4]
console.log(ary.includes(1));
이런식으로 사용할 수 있습니다.
찾을배열명 . includes (찾을내용); 그럼 true or false로 값을 반환해줍니다.
위와 같은 경우에는 1은 ary안에 존재하므로 true가 반환되는 것을 확인할 수 있습니다.
3. 참조 & 복사
우선, 참조와 복사의 개념부터 살펴보겠습니다.
참조(얕은복사)는, 원래의 값과 같은 값을 가르키는 값을 복사해오는 것이고, 즉 복사해온 값을 바꾸면 원래 값도 변함
복사(깊은복사)는, 원래의 값에 영향을 주지 않도록 완전히 새롭게 값을 복사해오는 것 입니다.
다시말해, 복사해온 값을 바꿔도 원래값에는 영향이 없는 것 입니다.
먼저 복사(깊은복사)부터 살펴보겠습니다.
//string, number, boolean; 원시값(primitive type);
let text = 'blockmonkey';
let copied = text;
copied = 'cat';
console.log(text , copied); // ->> blockmonkey cat
let num = 1;
let copied2 = num;
copied2 = 2;
console.log(num, copied2); // ->> 1 2
let bol = true;
let copied3 = bol;
copied3 = false;
console.log(bol, copied3); // ->> true false
이런식으로, 원래의 값을 복사해오고, 복사해온 값을 바꾸어줘도, 원래의 값에는 아무런 영향이 없습니다.
다음은 참조(얕은복사)를 살펴보겠습니다.
//객체, 배열, 함수; 객체임 셋다. // 참조관계;
let obj = {
name:'blockmonkey',
}
let copy1 = obj;
copy1.name = 'hamster';
console.log(copy1); // ->> { name: 'hamster' }
console.log(obj); // ->> { name: 'hamster' }
let ary = ['monkey'];
let copy2 = ary;
copy2[0] = 'cat';
console.log(copy2); // ->>['cat']
console.log(ary); // ->> ['cat']
let func = function(){};
func.abc = 'abc'; //함수안에 객체를 넣는 법. {abc : 'abc'}
let copy3 = func;
copy3.abc = 'def';
console.dir(copy3); // -> func {abc: 'def'}
console.dir(func); // ->> func {abc: 'def'}
이런식으로, 원래의 값이 복사해온 값과 함께 변하게 됩니다.
다시말해,
원시값(문자열, 수, 불리언)은 '복사(깊은복사)'를 하며,
객체(객체, 배열, 함수)는 '참조(얕은복사)'를 합니다.
그럼 어떻게 해결할 수 있는지, 객체에서도 '참조'가 아닌 '복사'를 해봅시다.
// //객체 복사방법 1(Object Key를 forEach로 가져와 복사하기) [원시값만 가능]
let obj = { a:1, b:2, c:3, d:4 }
let copy_obj = {};
Object.keys(obj).forEach(function(key){
copy_obj[key] = obj[key];
})
console.log(copy_obj);
객체를 복사하는 첫번째 방법입니다.
객체 내에 여러단계로 객체가 중첩되지 않고, 1단계 객체일 때만 유효합니다.
Object.keys라고 해서 원래 객체의 키값을 모두가져와, forEach문으로 돌려서 하나씩 대입해주는 방식입니다.
근데 이게 좀 불편하고 어렵다 싶으시면 다른 방법이 있습니다.
1단계 객체에서만 유효한것은 같습니다만, 아래식으로 쓰면 조금 더 간단하게 코드를 쓰실 수 있을 것 입니다.
Object.assign(copy_obj, obj);
그런데 객체 안에 객체가 있는 경우에는 어떻게 할까요?
//객체 복사방법 2 (JSON.parse(JSON.strinify(복사할객체)))
let obj = { a:1 , b: { c :2 }};
let copy = JSON.parse(JSON.stringify(obj));
obj.b.c = 4;
console.log(obj);
console.log(copy);
JSON.parse(JSON.stringify(복사할객체명) 을 삽입해서, 여러단계에 걸쳐져있는 2단계 3단계 객체들을 복사할 수 있습니다.
참조관계를 확인하는 방법은,
console.log(obj === copy);
이런식으로 같냐? 라고 컴퓨터한테 물어보면되요. 둘이 참조관계가 같다면 true가 출력될 것 이고(이 때는 얕은복사관계),
둘이 참조관계가 다른 깊은복사가 이루어졌다면, false가 출력됩니다.
배열도 유사한데요. 우선 1단계 배열에서는 어떻게 하는지 살펴봅시다.
let ary = [1,2,3];
let copy = ary.slice(); //깊은복사방법 1;
이렇게, slice()함수를 이용해서 모두 짤라내서 넣어주면됩니다. 그럼 더이상 원래의 ary와 연결고리가 사라지게 되죠.
중첩배열의 경우에는 어떨까요?
let ary = [1,2,[3,4,[5,6]]]
let copy2 = JSON.parse(JSON.stringify(ary)); //깊은복사방법 2;
위 객체의 방식과 똑같이 JSON을 이용해서 이중 삼중 배열을 복사할 수 있습니다.
*단, JSON.parse(JSON.stringify())의 경우에는 성능최적화가 최악이니, 최대한 사용을 지양합시다.
4. 팩토리패턴
바로 코드로 가봅시다 !
let card1 = {
name : 'monkey',
damage : 20,
hp : 100,
type : 'character', //중복발생.
attack : function(){
console.log(`공격 !`);
},
defence : function(){
console.log(`방어 !`);
},
}
let card2 = {
name : 'cat',
damage : 30,
hp : 80,
type : 'character',
attack : function(){
console.log(`공격 !`);
},
defence : function(){
console.log(`방어 !`);
},
}
let card3 = {
name : 'lion',
damage : 50,
hp : 200,
type : 'character',
attack : function(){
console.log(`공격 !`);
},
defence : function(){
console.log(`방어 !`);
},
}
위 카드들을 볼까요? name, damage, hp만이 유동적이며 그 아래 type, attack(), defence()는 모두 동일합니다.
그런데 이렇게 쓰면 너무나도 많은 중복이 발생해 상당히 번거로워집니다.
그럼 우리가 아는 함수를 통해서, 중복을 제거하는 작업을해봅시다.
//팩토리 패턴
const cardFactory = (name, damage, hp)=>{
return {
name: name,
damage: damage,
hp : hp,
type: 'character',
attack : function(){
console.log('공격 !');
},
defence : function(){
console.log('방어 !');
}
}
}
const man1 = cardFactory('blockmonkey', 1, 10);
const man2 = cardFactory('nero', 50, 100);
console.log(man1);
console.log(man2);
이렇게하면, name, damage, hp 등 유동적인 부분은 매개변수로 잡고, 외부에서 변수를 만들 때 직접적으로 만들 수 있죠.
그럼, 그 외에 중복이 발생하는 부분인, type, attack, defence등은 고정값으로 넣어줬기에 자동으로 생성됩니다.
이렇게 카드를 생성하는 공장을 만들어봤어요.
이런걸 보고 '팩토리 패턴(Factory Pattern)'이라고 불릅니다.
5. 프로토타입(Prototype)
우선 객체를 한번 찍어볼께요.
검사창에가서 콘솔에 var a = {} 라고 빈 객체를 생성하고, a를 출력해보겠습니다.
저는 분명 빈 객체를 생성했지만, 왠 __proto__라는 정체모를 것이 추가되었어요.
저 안에다가 정보를 넣고 그것을 다루는 것을 프로토타입이라고 부릅니다.
그럼 어떻게 쓰는건지 코드를 살펴보겠습니다.
let 프로토타입 = {
type: 'character',
attack: function(){
console.log(`공격 !`);
},
defence: function(){
console.log(`방어 !`)
},
};
let card1 = {
name : 'monkey',
damage : 250,
hp : 1000,
};
card1.__proto__ = 프로토타입;
console.log(card1);
프로토타입이라는 객체에는 공통된 것을 모두 몰아넣어줬습니다.
그리고 card1을 생성하면서 다른 부분을 넣어줬죠.
그리고 card1.__proto__ = 프로토타입 << 이렇게 적용시켜주면,
이렇게 프로토안에 우리가 만든 attack함수와 defence함수 그리고 type함수가 들어 있는 것을 확인할 수 있습니다.
그리고 접근할 때는, 평소처럼 card1.type , card1.attack()이런식으로 해주면 컴퓨터가 자동으로 찾아줍니다.
그럼 여기에 이번엔 팩토리패턴을 한번 적용시켜 보겠습니다.
let 프로토타입 = {
type: 'character',
attack: function(){
console.log(`공격 !`);
},
defence: function(){
console.log(`방어 !`)
},
};
let cardFactory = (name, damage, hp) => {
let card = {
name : name,
damage : damage,
hp : hp,
}
card._prototype__ = 프로토타입;
return card;
}
let man1 = cardFactory('monkey', 543, 1000);
let man2 = cardFactory('lion', 543, 1000);
let man3 = cardFactory('hamster', 543, 1000);
console.log(man1);
console.log(man2);
console.log(man3);
이런식으로하면, 팩토리패턴안에 프로토타입이 적용되지 않을까요?
그럼 만약에 기획자가 type을 'character'가 아닌 'toy'로 수정해달라고 요청했다고 칩시다.
그럼 우리는 오늘 학습한 참조관계와 객체의 수정을 활용해서 바꿀 수 있습니다.
let 프로토타입 = {
type: 'character',
attack: function(){
console.log(`공격 !`);
},
defence: function(){
console.log(`방어 !`)
},
};
let cardFactory = (name, damage, hp) => {
let card = {
name : name,
damage : damage,
hp : hp,
}
card._prototype__ = 프로토타입;
return card;
}
let man1 = cardFactory('monkey', 543, 1000);
let man2 = cardFactory('lion', 543, 1000);
let man3 = cardFactory('hamster', 543, 1000);
프로토타입.type = 'toy'; //프로토타입 수정하기
console.log(man1);
console.log(man2);
console.log(man3);
옆에 주석이 달린부분인, 프로토타입.type = toy 이 부분을 보세요.
그럼, 콘솔에 찍었을 때 참조관계에 따라서 모든 man1, man2, man3의 타입은 toy로 바뀝니다.
그럼 이번엔 기획자가 찾아와서,
크기도 있으면 좋겠는데? 'size' 항목도 추가해줘라고 했다고 가정합시다.
그럼 우리는 회심의 미소를 지으며,
프로토타입.size = 100
이 한줄을 적어주면, 모든 요소에 적용될 것 입니다.
마지막으로 하나만 더 !
우리가 지금까지 사용한 __proto__는 비표준이라서 자바스크립트에서 권장하지 않는 문법입니다.
그럼 어떻게하냐?
let 프로토타입 = {
type: 'character',
attack: function(){
console.log(`공격 !`);
},
defence: function(){
console.log(`방어 !`)
},
};
let cardFactory = (name, damage, hp) => {
let card = {
name : name,
damage : damage,
hp : hp,
}
Object.create(프로토타입) //card._prototype__ = 프로토타입; 비표준문법
return card;
}
이런식으로 Object.create(적용할프로토타입) << 이런식으로 해주시면 됩니다!
즉, 프로토타입이란, 객체간의 공유되는 내용을 따로 넣어두는 것 << 이라고 이해할 수 있습니다.
수도없이 많은 객체에 같은 프로토타입을 적용했다고 가정한다면,
프로토타입 하나만을 수정해서 수많은 여러 Factory들을 수정할 수 있을 것 입니다.
그럼 마무리로 카드 짝 맞추기 게임을 살펴보고 마치겠습니다.
6. 카드짝맞추기(영상)
7. 카드짝맞추기 (코드)
card.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>짝맞추기</title>
<style>
.card {
display: inline-block;
margin-right : 10px;
margin-bottom : 10px;
width: 115px;
height: 150px;
perspective: 200px;
}
.card-inner {
position: relative;
width: 100%;
height: 100%;
text-align: center;
transition: transform 0.8s;
transform-style: preserve-3d;
}
.card.flipped .card-inner {
transform: rotateY(180deg);
}
.card-front {
background-color: navy;
}
.card-front, .card-back {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
}
.card-back {
transform: rotateY(180deg);
border: 1px solid black;
background: gainsboro;
}
#wrapper{
width : 500px;
height : 100%;
}
</style>
</head>
<body>
<div id='wrapper'></div>
<script src="./card.js"></script>
</body>
</html>
card.js
let 가로 = 4;
let 세로 = 3;
let card_color_list = ['red', 'red', 'orange', 'orange', 'powderblue', 'powderblue', 'green', 'green', 'yellow', 'yellow', 'pink', 'pink']
let color_list = card_color_list.slice(); // 백업.
let card_color = [];
let click_flag = true;
let click_card = []; // 클릭수를 계산.
let finished_card = []; //완료된 카드.
let start_time; //게임시작시간
//카드색깔 섞기;
//피셔에이츠방식
const card_shuffling = () => {
for(let i=0; 0 < card_color_list.length; i++){
card_color = card_color.concat(card_color_list.splice(Math.floor(Math.random() * card_color_list.length), 1));
}
}
const card_setting = (가로, 세로) => {
click_flag = false;
//카드 12장을 만들기 위한, 반복문.
for(let i=0; i < 가로*세로; i++){
//card(div) 안에 cardInner(div)를 만들고, 그안에 cardFront(div) & cardBack(div) 생성.
let card = document.createElement('div');
let cardInner = document.createElement('div');
let cardFront = document.createElement('div');
let cardBack = document.createElement('div');
//클래스네임을 추가하는 또다른 방법. (classList.add('className') << 상이함)
card.className = 'card';
cardInner.className = 'card-inner';
cardFront.className = 'card-front';
cardBack.className = 'card-back';
//색깔넣기
cardBack.style.backgroundColor = card_color[i];
//cardInner안에 cardFront와, Back을 넣고, 그것을 card안에 넣고, 그 뒤에 card를 body에 추가함.
cardInner.appendChild(cardFront);
cardInner.appendChild(cardBack);
card.appendChild(cardInner);
document.querySelector('#wrapper').appendChild(card);
//클로저 해결. 이벤트리스너 toggle을 달아서 flipped클래스를 넣었다 뻇다함.
((card)=>{
card.addEventListener('click', ()=>{
if(click_flag && !finished_card.includes(card)){ // ******
card.classList.toggle('flipped');
click_card.push(card); //클릭카드에 카드를 추가함.
//두개 카드 색깔 대조.
if(click_card.length === 2){
let clicked_card_one = click_card[0].querySelector('.card-back').style.backgroundColor;
let clicked_card_two = click_card[1].querySelector('.card-back').style.backgroundColor;
console.log(clicked_card_one, clicked_card_two);
if(clicked_card_one === clicked_card_two){ //클릭한 카드 색깔이 같을 때,
finished_card.push(click_card[0]);
finished_card.push(click_card[1]);
click_card = []; //클릭한 카드 초기화
if(finished_card.length === 가로*세로){
let end_Time = new Date();
let time = (end_Time - start_time) / 1000
alert(`축하합니다 성공했습니다. ${time}초 걸렸습니다!`);
document.querySelector('#wrapper').innerHTML = ''; //게임 초기화;
finished_card = [];
card_color = [];
card_color_list = color_list.slice(); // ******
start_time = null;
card_shuffling();
card_setting(가로, 세로); // 게임 재시작 호출.
}
} else { //두카드의 색깔이 다를 때,
click_flag = false;
setTimeout(function(){
click_card[0].classList.remove('flipped');
click_card[1].classList.remove('flipped');
click_flag = true;
start_time;
click_card = []; //클릭한 카드 초기화
}, 1000);
}
}
}
})
})(card)
}
//처음 유저에게 카드 외울시간 주기. 카드 열어두기(즉, 뒤집어두기)
//setTimeout을 통해, 카드가 순서대로 뒤집어지는 효과 추가함. 1000-> 1100 -> 1200 이렇게 순차적으로 적용될 수 있도록.
document.querySelectorAll('.card').forEach((cards, idx)=>{
setTimeout(function(){
cards.classList.add('flipped');
}, 1000 + 100 *idx);
//다시 뒤집기
setTimeout(function(){
cards.classList.remove('flipped');
click_flag = true; //셋팅이 끝나고 클릭할 수 있도록 플레그 설정.
start_time = new Date();
}, 5000);
});
}
card_shuffling();
card_setting(가로, 세로);
게임코드를 리뷰하지 않으니 간단하게 주석을 달아두었습니다.
참고하시면 좋을 것 같습니다.
'Web Development > Front-end' 카테고리의 다른 글
[Zerocho-13] 자스스톤 (Call by Value, Constructor, this, 엄격모드) (0) | 2020.10.27 |
---|---|
[Javascript] TodoList (0) | 2020.10.26 |
[Zerocho-11] 틱택토 심화 ( 스코프 문제를 해결하는 3가지 방법 ) (0) | 2020.10.23 |
[Zerocho-10] 반응속도 테스트 게임 ( 호출스택, new Date() ) (0) | 2020.10.23 |
[Zerocho-09] 지뢰찾기 (ES6 & Scope & Closure & IFFE...) (0) | 2020.10.21 |