본문 바로가기

Web Development/Front-end

[Zerocho-12] 카드 짝 맞추기 (toggle, includes, 참조&복사, 팩토리패턴&프로토타입)

안녕하세요.

오늘은 카드 짝맞추기 게임을 하면서 배웠던 주요 내용들을 다뤄보며 복습하는 시간을 가지려고 합니다.

바로 시작하겠습니다.

 

1. classList.toggle('ClassName');

토글은, classList.add() 와 classList.remove() 두 함수를 묶은 것입니다.

즉, 클릭하면 클래스를 생성해주고, 다시 클릭하면 클래스를 삭제해주는 역할까지 합니다.

 

잘 이해가 안되시면 동영상을 참조해주세요.

 

toggle

코드를 한번 살펴보겠습니다.

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(가로, 세로);

게임코드를 리뷰하지 않으니 간단하게 주석을 달아두었습니다.

참고하시면 좋을 것 같습니다.