본문 바로가기

Web Development/Front-end

[Zerocho-09] 지뢰찾기 (ES6 & Scope & Closure & IFFE...)

중급강좌에서는 코드가 매우 길어지므로 게임 포스팅 보다는,

다루었던 주요 이론 내용을 보충하는 방식으로 진행할 예정이니,

복습용으로만 참고하시고,

 

중급강좌는 제로초님의 동영상으로 시청하실 수 있도록 링크 남기겠습니다.

(아래 링크 (8-1) 부터~)

www.youtube.com/watch?v=Qq2IJ2iEgWA&list=PLcqDmjxt30Rtbxbh4eJREOVekql_kWVmu

 

1. ECMA Script6(ES6)

ES 6는 ECMA 라는 국제기구에서 만든 6번째 표준 스펙입니다.

즉, Javascript의 6번째 버전이라고 생각하시면 됩니다.

ES6는 2015년에 출시되어 ES2015라고도 불립니다.

2016년에는 ES7, 2017년에는 ES8 등등 이런식으로 말이죠.

 

그런데 왜 2015년 버전인, ES6를 다루느냐?

왜냐하면, Javascript는 ES5 이전의 문법과 ES6이후의 문법이 확연한 차이를 보이기 때문입니다.

예를들어,게임으로치면 대규모업데이트를 했는데 그게 ES6라고 보시면됩니다.

ES7, ES8 등등은 기능 추가 등과 같은 작은 업데이트였다고 보시면되구요.

따라서, ES6를 숙지해두신다면, 그 이후의 추가적인 업데이트사항은 경험을 통해 알아가면 되겠죠?

 

아무튼! 서론은 기까지하고, ES6에서 달라진 내용을 좀 살펴봅시다.

 

우선, 변수선언방법입니다.

//ES5
var a = 1;

//ES6 변수
let a = 1;

//ES6 상수
const a = 1;

 

기존에 우리가 계속해서 써왔던 ES5식 문법으로는 변수를 선언할 때, var를 붙여서 선언해왔습니다.

 

그런데 ES6에서는 let과 const가 등장했죠.

let은 말그대로 변수(변할수 있는 수)를 선언하는 방법입니다. 다만, var과의 차이점은,

var는 같은 이름의 변수를 재선언이 가능하나, let의 경우에는 같은이름의 변수를 재선언했을 때 오류를 뿜는다는 것이죠.

 

예를 들어보겠습니다.

//var

var a = 1;

var a = 2;
//오류없이 a는 2가 됨.



//let

let a = 1;
let a = 2;
//위에서 이미 a를 선언해뒀는데, 재선언하므로 오류가 발생함!

 

다음으로 const의 등장입니다. 기존에는 var 하나로 모든것을 해결했으나 ! Javascript에도 드디어 상수의 개념이 생겨납니다.

상수는, 변할 수 없는 수입니다. 즉, 한번 선언하면 다시 이 값을 변경할 수 없다는 것이죠.

예를 봅시다.

//변수 var
let a = 2;
a = 4;

console.log(a); //=> a의 값은 4로 출력됩니다.



//상수 const
const a = 2;
a = 4;

console.log(a); //=> 오류가 발생합니다. 즉, 변수의 값을 한번 선언하면 그 뒤로 바꿀 수 없습니다.

 

 

다음으로 알려드릴 내용은 ES6에 추가된 함수(화살표함수)입니다.

우선 바로 살펴보겠습니다.

//ES5 스타일
function printHello(name){
	console.log(`${name} Hello !`);
}

printHello(`blockmonkey`);




//ES6 스타일
const printHello = (name) => {
	console.log(`${name} Hello !`);
}

printHello(`blockmonkey`);



//ES6 생략식
const printHello = (name) => console.log(`${name} Hello!`);

printHello(`blockmonkey`);

ES5에서는 function 함수명(매개변수){ return값 } 이런식으로 선언했다면, 

ES6에서는 변수선언 = (매개변수) => { return값 } 이런식으로 작성합니다.

ES6식 표기법에서는, 한 줄 return값이 있을 때는 { } 를 생략할 수 있습니다.

 

*TIP

그리고 이건 경험에서 팁인데..

ES6식 함수표현식에서는 this가 먹히지 않습니다.(왜 인지 이유는 몰라요..ㅠ 경험상 그렇더라구요)

그리고 ES5식 함수표현식에서는 this를 사용할 수 있습니다. 따라서, this를 사용하는 경우에는, ES5식 으로 작성하는 것을 권장드립니다.

 

 

 

자 이제 ES6를 배워봤고, 다시 지뢰찾기내용으로 돌아가서, 지뢰찾기에서 주로 다루는 내용의 문법들을 알아보겠습니다.

 

2. 스코프(Scope)

우선 알아볼 내용은 스코프입니다.

스코프는 한국어로 '범위' 라고 해석되며, 변수 혹은 함수의 범위에 대한 내용입니다.

한 가지를 기억하세요

"안에서는 밖을 볼 수 있지만, 밖에서는 안을 볼 수 없다." (by.드림코딩)

let a = 'Dog';

const example = () => {
    let b = 'Cat';
    console.log(a);
}



example(); // Dog;
console.log(b); // ???

a라는 변수를 한개 선언했습니다. 그리고 그걸 함수 내에서 a를 콘솔에 찍어라 ! 라고 명령했쬬.

example함수를 실행시켰더니 컴퓨터는 a변수를 찾아서 Dog를 출력했습니다.

그런데말이죠, 함수내에서 변수 b 를 만들어, 외부에서 console.log(b)를 출력했더니 오류가 발생했습니다.

이런 것이 스코프입니다.

외부에서 선언한 a변수는 전역변수로 언제 어디서나 사용이 가능한 변수이지만,

함수내부에서 선언한 b변수는 지역변수로 함수 내에서만 유효한 변수입니다.

만약 console.log(b)를 함수 내에서 했으면 정상 동작했겠쬬?

즉, 함수내에서는 외부의 변수(전역변수)를 읽을 수 있지만, 전역스코프에서, 함수내에있는 지역변수를 읽을 수는 없습니다.

 

한가지 예를 더 봅시다.

let x = 'Dog';
function ex(){
    x = 'Cat';
    console.log(`first console => ${x}`);
}

ex();
console.log(`Second console => ${x}`);

let x로 'dog'를 선언했고,

ex()라는 함수가 선언되었습니다.

그리고 함수내에서 x값을 cat으로 바꿔줫어요. 그리고 콘솔로 x를 출력하라고 했죠.

그럼 첫콘솔과 둘째 콘솔의 값은 어떻게될까요? 첫콘솔의 x가 가르키는 값과, 둘째 콘솔이 가르키는 x의 값은 무엇일까요?

첫 콘솔에서 가르키는 x의 값은 얼핏보면, function 내에 있는 x='Cat' 이라고 생각할 수 있지만, 이는 선언이아닙니다. 즉, 첫 콘솔내 x의 값은 전역스코프에 있는 let x = 'Dog'입니다. 그런데 출력결과를 보면, 'Cat'이라고 나옵니다. 왜냐하면, function 내에서 x의 값을 'Cat'으로 바꿔줬기 때문입니다. 여기서 중요한점은, console.log(x) << 이 x가 가르키는 포인터 지점은 첫줄에있는 let x = 'Dog'; 이라는 점입니다.

그리고 두번째 콘솔에있는 x는 당연히 스코프의 유효범위에 따라, function 내에 있는 x 일 수 없고, 전역스코프에있는 x일 것입니다.

 

조금 바꿔볼까요?

 

let x = 'Dog';
function ex(){
    let x = 'Cat';
    console.log(`first console => ${x}`);
}

ex();
console.log(`Second console => ${x}`);

function 내부에 x를 선언하고 'Cat'으로 바꿧습니다.

그럼 이제 첫번째 콘솔에서의 x값은 누굴 가르킬까요?

function() 내부에있는 let x = 'Cat'; 이 부분일겁니다. 왜냐하면, 우선 스코프(범위)내에 있는 변수를 탐색했는데, 찾아냈기 때문입니다. 이전예제에서는 선언하지 않았기 때문에 이는, 스코프내에 변수가 있다고 판단할 수 없어 전역스코프를 탐색했으나, 현재 예제에서는 let x = 'Cat'이라고 선언함으로써, function() 내에 변수 x가 존재하기 때문입니다.

 

 

한 발 더 나아가 좀 더 자세히 들여다 볼까요?

스코프체인

//스코프체인
//inner 내부에서 변수를 탐색 -> outer 내부에서 변수를 탐색
//-> 없으면, 전역범위에서 변수를 탐색
//이렇게 타고 올라가서 찾는다해서, 스코프체인이라부름.

const name = 'monkey';
function outer(){
    console.log('바깥', name);
    function inner(){
        console.log('안', name);
    }
    inner();
}

outer();

name 이라는 변수를 선언하고, outer라는 함수를 선언했습니다. 그리고 그 안에, inner라는 함수를 선언했죠.

그리고 둘다 consol에 name이라는 값을 찍도록 했습니다.

그럼 이 둘은 모두 monkey라는 값을 찍겠죠?

 

그런데 그 과정이 어떻게되냐면, 

outer함수를 선언한 부분에서 inner함수가 내부에서 선언되었고 그리고 inner()함수를 실행시켜줬죠?

그럼, 저 outer함수는 실행할때, outer함수 내에, name이라는 변수가 있는지 먼저 찾습니다. 그런데 없죠?

그럼 이제 전역스코프로 올라가서, name변수를 찾습니다. 그럼 맨위에 있는 const로 선언한 name변수를 찾을 수 있습니다.

 

이제 inner함수가 실행되는데, inner함수도 마찬가지로, name변수를 찾아야합니다.

우선 inner()함수 내에 name변수가 있는지 확인합니다. 그런데 없습니다.

그럼 다시 한 스코프 올라가서 outer에서 name변수가 존재하는지 찾습니다.

그런데, 없어요. 그럼 또 다시 한 스코프 올라가서 이제 전역스코프입니다. 여기서 name변수를 찾아, monkey값을 찾아냅니다.

이렇게 타고올라가는 흐름이라고 해서 스코프체인이라고 이름이 붙었습니다.

 

한발짝만 더 딥다이브 해보겠습니다 

 

 

렉시컬 스코프(정적스코프)

말만들어도 어렵죠? 렉시컬?뭐지?

lexical (언어의) 라는 의미입니다.

말 그대로 "언어(코드)를 선언한 동시에 스코프가 정해진다"라는 의미입니다.

그냥 스코프 복습한다 생각하세요. 같은 개념 확장판느낌입니다.

바로 예제보고 설명할께요.

var name = 'zero';
function log(){
    console.log(name); //얘는 무조건 1번에 있는 name값을 가르킨다.
}
function wrapper(){
    name = 'nero';
    log();
}
wrapper();

변수 name을 만든 뒤에, log라는 함수를 선언하고 콘솔로 name을 찍도록 작성했어요. 그런데 호출은 안했죠?

그리고, function wrapper()라는 함수를 만들고, 그 안에서 name값은 nero라고 바꿔주고, log를 호출했어요.

그럼 wrapper를 호출했을 때 , name의 값은 무엇일까요?

 

var name = 'zero'가 선언되고,

wrapper()를 호출했으니 , name값을 'nero'로 바꿔주고, log()함수를 실행할 것입니다.

그럼 log함수는 콘솔에 name값을 찍으려할거고,

'zero' 에서 'nero'로 바뀐 값이 출력될 것 입니다.

즉, log() var name= 'zero'라고 선언한 이 값을 바라보고있는데, wrapper에서 이를 nero로 바꾸어 줬기에 nero가 출력될 뿐,

log()함수 내 name값이 바라보는 타게팅 값은 변치 않았습니다.

이게 핵심이에요.

다른 함수끼리의 영향은 없습니다. 바라보는 값은 변치 않아요.

 

한가지 예제를 더 보겠습니다.

var name = 'zero';
function log(){
    console.log(name); //얘는 무조건 1번에 있는 name값을 가르킨다.
}
function wrapper(){
    var name = 'nero';
    log();
}
wrapper();
console.log(name);

이렇게 한번 해볼까요?

에디터가 있다면 한번 복붙해보세요. wrapper()함수 내에 선언한 name값은 호출한 적이 없다 라고 어두운 색으로 나올 것입니다.

 

즉, 다시말해 코드를 작성과 동시에 스코프는 이미 정해져 있는 '정적'인 특성을 가집니다.

 

대부분의 언어는 렉시컬스코프의 특성을 띄지만 안그런 언어도 있긴 합니다.

만약 렉시컬스코프가(정적 스코프)특성이 아닌 (동적스코프)라면?

wrapper()내에서 log()를 호출했기에, log() name값은 log()내부의 name을 탐색하고 -> 바로 상위의 스코프인 wrapper()함수의 스코프를 탐색해 var name = 'nero' 라는 값으로 인식할 것입니다.

 

 

자 여기까지 스코프에 대한 설명입니다.

이런 스코프의 특성을 가지고, 비밀변수(밖에서는 읽을 수 없는 변수)를 생성하는 등의 일을 할 수 있고,

이런 특성을 이해해야만, 변수를 오사용하는 오류를 방지 할 수 있습니다.

 

다음은, 클로저 문제입니다.

 

 

3. 클로저(Closure)

클로저 문제는 반복문-비동기를 함께 사용할 때 주로 일어납니다.

바로 예제를 통해 이해해보겠습니다.

//이미 100까지 반복문이 돌고, 비동기코드가 실행됨.
for(var i=0; i<10; i+=1){ //0.000000000001초
    setTimeout(function(){
        console.log(i);
    }, i*1000);
}

위와같은 코드가 있습니다.

어떤 결과가 나올까요? 예상하기로는,

0 -> 1 , 2, 3, 4, 5, 6, 7, 8, 9 << 이런식으로 1초 2초 3초 4초 5초 6초 ...마다 콘솔이 출력될 것입니다.

비동기는 말그대로 순서대로 실행하지 않는 코드입니다. 여기서는 setTimeout이 비동기에 해당되죠.

그리고 그 비동기코드의 console값을 1~10까지 입력하는것은 매우 번거로운 일이기에 반복문을 이용했습니다.

그런데, 예상과 달리 1~10이 나오지 않고, 10만 나와요 계속..

 

왜그럴까요?

 

왜냐하면, for문 0~10까지 도는 속도는 setTimeout(비동기)가 실행되기 전에 이미 모든연산이 끝났기 때문입니다.

따라서 i 의 값은 setTimeout을 호출할 때 이미 10이 되어버렸다는 것이죠.

따라서, 10만 출력되는 것입니다.

 

이런문제는 우리가 로또게임을만들면서도 겪은적이 있습니다. 한번 살펴볼까요?

//로또 추첨기 중...
setTimeout(function(){
    ball_color(0);
}, 1000);
setTimeout(function(){
    ball_color(1);
}, 2000);
setTimeout(function(){
    ball_color(2);
}, 3000);
setTimeout(function(){
    ball_color(3);
}, 4000);
setTimeout(function(){
    ball_color(4);
}, 5000);
setTimeout(function(){
    ball_color(5);
}, 6000);

이 부분입니다. ball_color라고 하는 로또 번호를 생성하고 색을 칠하고 디자인해주는 함수를 1초마다 실행하게 해줬죠?

이걸 어떻게 해결할까요?

var picked_number = shuffled
                    .slice(0, 6)
                    .sort(function(p,c){
                        return p-c
                    });

for(var i=0; i<picked_number.length; i++){
    setTimeout(function(){
        ball_color(picked_number[i]);
    }, (i+1) * 1000 );
}

이런 식으로 하면, 위와 같은 코드를 만들 수 있지 않을까요?

그런데 스코프 문제로 인해, 이와 같이 입력하면 i의 값은 늘 picked_number에 들어있는 6개의 엘리먼트로인해 5가 출력될것입니다.

그럼 우리가 원하는 값이 아니죠?

따라서 이런 반복문 - setTimeout(비동기)코드의 조합은 실패사례로 남았고 다루지 않았습니다.

 

 

그럼 이를 어떻게 해결할까요?

여기에 클로저를 적용해 해결해보겠습니다.

for(var i=0; i<picked_number.length; i += 1){
    function 클로저적용(j){
        setTimeout(function(j){
            ball_color(picked_number[j]);
        }, (j+1) * 1000);
    }
    클로저적용(i);
}

클로저를 다음과 같이 활용하면 됩니다.

기존에 있던 setTimeout의 콜백함수내에서 원래 매개변수로 i의 값을 받았는데 적용이 안됐잖아요?

그러므로 새 클로저 function을 만들어 j를 인자로 받게하고, 그것을 비동기가 아닌 부분에서 호출함으로써 i값을 j값으로 받아옵니다.

그리고, 그것을 setTimeout의 매개변수로 준다면 이를 적용할 수 있지않을까요?

편법같지만, 원래 이렇게 사용하는거라고 하네용;;ㅋㅋ

 

 

 

 

 

4. IFFE

그런데 실제로는 이 클로저함수를 저렇게사용하면 좀 보기 번거로워서

IFFE를 적용한다고 해요.

"함수를 선언과 동시에 실행시키는 방법"입니다.

 

(function sayHello(){
	console.log("Hello");
})();

sayHello라는 함수를 만들고, 그것을  ( ) << 이 괄호로 묶어줬어요. 그리고 함수를 실행하듯이 뒤에 ()를 붙여줬구요.

이런식으로 똑같이 클로저에도 적용합니다.

 

for(var i=0; i<picked_number.length; i++){
    (function 클로저(j){
        setTimeout(function(){
            ball_color(picked_number[i]);
        }, (i+1) * 1000 );
    })(i);
}

조금 더 단순해졌죠?

 

 

 

5. 재귀함수

//재귀함수
//반복문을 함수로 표현. 함수 자신을 호출하여 구현
//사람이 이해하기 쉽고, 컴퓨터가 이해하기 어려움, (반복문은 컴퓨터친화형)

function 재귀함수(i){
    console.log(i);
    if(i < 5){        
        재귀함수(i+1);
    }
}

재귀함수(1);

재귀함수는 반복문을 대신해 쓸 수 있는 함수입니다.

재귀함수() 를 만들어 매개변수로 i값을주고, 콘솔에 i를 찍는 함수를 만들었습니다.

그리고 if문 아래에 보시면, 재귀함수(i+1); << 이렇게 스스로를 호출하고 있습니다.

스스로를 호출하면서 i 값을 1씩 증가시키죠.

근데, if문에서 5보다 작을때까지만 true라는 조건을 부여했습니다.

따라서, 아래에서 재귀함수(1); 이라고 재귀함수를 호출하면,

console.log(1);

console.log(2);

console.log(3);

console.log(4);

이렇게 총 네번을 출력합니다. 마치 반복문과 같지 않나요? 이러한 재귀함수는 주로 사람이 코드를 볼 때 조금 더 가독성 좋게 보기 위해 활용하며, 여기에서 로직을 구현할 때는 비교적 컴퓨터입장에서는 많은 자원을 소모하므로 로직을 잘 구현해 줄 필요가 있습니다.

 

6. target & currentTarget

//e.target VS e.currentTarget;
//Current Target은 이벤트리스너가 달린 태그.
//target은 실제 이벤트가 발생한 태그.

tbody.addEventListener('click', function(e){
    console.log('Current Target =>' , e.currentTarget);
    console.log('e.target =>', e.target);
})

이렇게 두개를 출력해보면 쉽게 알 수 있는데요.

currentTarget은 이벤트리스너가 달린 태그인 tbody를 의미하며, target은 실제 이벤트가 발생한 태그입니다.

따라서 둘은 다를 수 있으며, 주로 currentTarget을 선호한다고 합니다.

 

 

 

7. 우클릭 이벤트(contextmenu)

//기존사용하던, 클릭이벤트
tag.addEventListener('click', function(e) {
	....

}


//우클릭시 발생하는 이벤트
tag.addEventListener('contextmenu', function(e) {
	....
}

 

8. classList.add & classList.remove

let tag = document.querySelector('#hi');
let create_button = document.querySelector('.btn_create');
let delete_button = document.querySelector('.btn_delete');


create_button.addEventListener('click', function(e){
    tag.classList.add('flag');
})

delete_button.addEventListener('click', function(e){
    tag.classList.remove('flag');
})

create버튼을 누르면, classList.add('flag') 라는 코드가 실행되는데, 이 코드는 해당 태그에 클래스를 달아주는 역할을 합니다.

그리고, classList.remove('flag') 라는코드 부분은 반대로, 해당 태그에 'flag' 클래스를 삭제해주는 역할을 합니다.

 

 

 

 

 

---배열 Review---

이 부분은 그냥 제가 헷깔려서 공부한 김에 추가합니다.

필요하신분들만 참고하세용.

9. splice & slice

//splice - 배열을 삭제, 수정할 수 있는 만능 맥가이버?!
splice(시작idx, 지울갯수, "추가요소");

let ary = [1,2,3,4];

let a = ary.splice(0, 1, '사과');


console.log(a); // -> [ 1 ]
console.log(ary); // -> [ '사과', 2, 3, 4 ]


//slice - 배열을 복사함

let ary = [1,2,3,4];

let a = ary.slice(0,2);


console.log(a); // [1,2]
console.log(ary); // -> [1,2,3,4]

 

splice는 배열을 삭제 혹은 수정할 수 있는 만능 맥가이버입니다. ( 단, 원래 배열을 훼손합니다. )

slice는 배열을 복사하는 역할을 합니다. ( 이는 원래 배열을 손상시키지 않습니다. )

 

 

10. concat

let arr = [1,2]
let arr2 = [3,4]

let concated = arr.concat(arr2);

console.log(concated); // -> [ 1, 2, 3, 4 ]
console.log(arr, arr2); // -> [ 1, 2 ] [ 3, 4 ]

concat은 배열을 합치는 역할을 합니다.

위에서는 간단하게 두개의 배열만을 합쳤지만, 더 많은 숫자의 배열 여러개를 concat(ary1, ary2, ary3) 다음과 같이 추가할 수 있습니다.

 

 

11. filter

let ary = [1, '사과', '포도', 2, 3, 4];

let a = ary.filter((v) => {
    return typeof(v) === 'string';

})

우선 배열을 ary로, 숫자와 문자가 섞인 배열을 하나 만들어줬습니다. 그런데, 저 중에 저는 '사과'와 '포도' 처럼 문자열의 데이터만 추출하고 싶다면, filter함수를 이용해 필터링합니다. 즉, filter함수는 배열내에서 내가 원하는 자료형만 가져오는 역할을 합니다.

 

 

 

 

지뢰찾기 영상 & 코드

코드가 너무 길어서 코드분석설명을 따로 하지 않았습니다.

코드만 올립니다.

 

지뢰찾기 게임영상

 

 

 

지뢰찾기 코드(HTML)

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title>지뢰찾기</title>
        <style>
            table {
                border-collapse: collapse;
            }
            td {
                border: 1px solid #bbb;
                text-align: center;
                line-height: 20px;
                width: 20px;
                height: 20px;
                background: gray;
            }
            td.opened {
                background: white;
            }
            td.flag {
                background: red;
            }
            td.question {
                background: orange;
            }
        </style>
    </head>
    <body>
        <input id="hor" type="number" placeholder="가로" value="10">
        <input id="ver" type="number" placeholder="세로" value="10">
        <input id="mine" type="number" placeholder="지뢰" value="20">
        <button id="exec">실행</button>
        <table id="table">
            <thead>
                <tr>
                    <td><span id="timer">0</span>초</td>
                </tr>
            </thead>
            <tbody></tbody>
        </table>
        <div id="result"></div>
        <script src="mine.js"></script>
    </body>
</html>

 

지뢰찾기 코드 (JS)

var dataset = [];
var tbody = document.querySelector('#table tbody');
var 중단플래그 = false;
var 열은칸 = 0;
var 코드표 = {
  연칸: -1,
  물음표: -2,
  깃발: -3,
  깃발지뢰: -4,
  물음표지뢰: -5,
  지뢰: 1,
  보통칸: 0,
};

document.querySelector('#exec').addEventListener('click', function() {
    //초기화작업.
  tbody.innerHTML = '';
  document.querySelector('#result').textContent = '';
  dataset = [];
  열은칸 = 0;
  중단플래그 = false;

  //칸만들기 가로값, 세로값, 지뢰값.
  var hor = parseInt(document.querySelector('#hor').value);//가로
  var ver = parseInt(document.querySelector('#ver').value);//세로
  var mine = parseInt(document.querySelector('#mine').value);//지뢰


  //배열생성 가로 * 세로 만큼 그리고, 인덱스값으로 그 값을 채움.
  var 후보군 = Array(hor * ver)
    .fill()
    .map(function (요소, 인덱스) {
      return 인덱스;
    });

//지뢰값 설정을 위한 셔플링.
  var 셔플 = [];
  while (후보군.length > hor * ver - mine) {
    var 이동값 = 후보군.splice(Math.floor(Math.random() * 후보군.length), 1)[0];
    셔플.push(이동값);
  }

  //이차원배열 생성
  for (var i = 0; i < ver; i += 1) {
    var arr = [];
    var tr = document.createElement('tr');
    dataset.push(arr);
    for (var j = 0; j < hor; j += 1) {
      arr.push(코드표.보통칸);
      var td = document.createElement('td');

      //우클릭 이벤트리스너 (contextmenu)
      td.addEventListener('contextmenu', function (e) {
        e.preventDefault();
        if (중단플래그) {
          return;
        }
        //이차원배열에서, 클릭이벤트 위치 찾아내기.
        var 부모tr = e.currentTarget.parentNode;
        var 부모tbody = e.currentTarget.parentNode.parentNode;
        //프로토타입은 아직모르겠음.
        var 칸 = Array.prototype.indexOf.call(부모tr.children, e.currentTarget);
        var 줄 = Array.prototype.indexOf.call(부모tbody.children, 부모tr);
        if (dataset[줄][칸] === 코드표.연칸) {
          return;
        }
        if (e.currentTarget.textContent === '' || e.currentTarget.textContent === 'X') {
          e.currentTarget.textContent = '!';
          e.currentTarget.classList.add('flag');
          if (dataset[줄][칸] === 코드표.지뢰) {
            dataset[줄][칸] = 코드표.깃발지뢰;
          } else {
            dataset[줄][칸] = 코드표.깃발;
          }
        } else if (e.currentTarget.textContent === '!') {
          e.currentTarget.textContent = '?';
          e.currentTarget.classList.remove('flag');
          e.currentTarget.classList.add('question');
          if (dataset[줄][칸] === 코드표.깃발지뢰) {
            dataset[줄][칸] = 코드표.물음표지뢰;
          } else {
            dataset[줄][칸] = 코드표.물음표;
          }
        } else if (e.currentTarget.textContent === '?') {
          e.currentTarget.classList.remove('question');
          if (dataset[줄][칸] === 코드표.물음표지뢰) {
            e.currentTarget.textContent = 'X';
            dataset[줄][칸] = 코드표.지뢰;
          } else {
            e.currentTarget.textContent = '';
            dataset[줄][칸] = 코드표.보통칸;
          }
        }
      });
      //우클릭 이벤트리스너 (click)
      td.addEventListener('click', function (e) {
        if (중단플래그) {
          return;
        }
        var 부모tr = e.currentTarget.parentNode;
        var 부모tbody = e.currentTarget.parentNode.parentNode;
        var 칸 = Array.prototype.indexOf.call(부모tr.children, e.currentTarget);
        var 줄 = Array.prototype.indexOf.call(부모tbody.children, 부모tr);
        if ([코드표.연칸, 코드표.깃발, 코드표.깃발지뢰, 코드표.물음표지뢰, 코드표.물음표].includes(dataset[줄][칸])) {
          return;
        }
        e.currentTarget.classList.add('opened');
        열은칸 += 1;
        if (dataset[줄][칸] === 코드표.지뢰) {
          e.currentTarget.textContent = '펑';
          document.querySelector('#result').textContent = '패배하셨습니다.';
          중단플래그 = true;
        } else {
          var 주변 = [
            dataset[줄][칸-1], dataset[줄][칸+1],
          ];
          if (dataset[줄-1]) {
            주변 = 주변.concat([dataset[줄-1][칸-1], dataset[줄-1][칸], dataset[줄-1][칸+1]]);
          }
          if (dataset[줄+1]) {
            주변 = 주변.concat([dataset[줄+1][칸-1], dataset[줄+1][칸], dataset[줄+1][칸+1]]);
          }
          var 주변지뢰개수 = 주변.filter(function(v) {
            return [코드표.지뢰, 코드표.깃발지뢰, 코드표.물음표지뢰].includes(v);
          }).length;
          // 거짓인 값: false, '', 0, null, undefined, NaN
          e.currentTarget.textContent = 주변지뢰개수 || '';
          dataset[줄][칸] = 코드표.연칸;
          if (주변지뢰개수 === 0) {
            var 주변칸 = [];
            if (tbody.children[줄-1]) {
              주변칸 = 주변칸.concat([
                tbody.children[줄 - 1].children[칸 - 1],
                tbody.children[줄 - 1].children[칸],
                tbody.children[줄 - 1].children[칸 + 1],
              ]);
            }
            주변칸 = 주변칸.concat([
              tbody.children[줄].children[칸 - 1],
              tbody.children[줄].children[칸 + 1],
            ]);

            if (tbody.children[줄+1]) {
              주변칸 = 주변칸.concat([
                tbody.children[줄 + 1].children[칸 - 1],
                tbody.children[줄 + 1].children[칸],
                tbody.children[줄 + 1].children[칸 + 1],
              ]);
            }
            주변칸.filter(function (v) {
              return !!v;
            }).forEach(function(옆칸) {
              var 부모tr = 옆칸.parentNode;
              var 부모tbody = 옆칸.parentNode.parentNode;
              var 옆칸칸 = Array.prototype.indexOf.call(부모tr.children, 옆칸);
              var 옆칸줄 = Array.prototype.indexOf.call(부모tbody.children, 부모tr);
              if (dataset[옆칸줄][옆칸칸] !== 코드표.연칸) {
                옆칸.click();
              }
            });
          }
        }
        if (열은칸 === hor * ver - mine) {
          중단플래그 = true;
          document.querySelector('#result').textContent = '승리했습니다! ^^';
        }
      });
      tr.appendChild(td);
    }
    tbody.appendChild(tr);
  }
  // 지뢰 심기
  for (var k = 0; k < 셔플.length; k++) { // 60
    var 세로 = Math.floor(셔플[k] / ver);  // 6
    var 가로 = 셔플[k] % ver; // 0
    tbody.children[세로].children[가로].textContent = 'X';
    dataset[세로][가로] = 코드표.지뢰;
  }
});