제로초 JavaScript 강의 - 로또 추첨기 순서도 그리기, 무작위로 공 뽑기, 공 정렬하기, 일정 시간 후에 실행하기, var과 let의

Study/JavaScript

 

  • 제로초 JavaScript 강의 - 로또 추첨기 순서도 그리기, 무작위로 공 뽑기, 공 정렬하기, 일정 시간 후에 실행하기, var과 let의 차이



Review
 
 
 오늘까지만 제로초님의 강의를 듣고 다른 강의로 머리를 환기시키기로 했다.
순서도 그리기를 시작하고부터 머리가 마비되는 것 같은 기분이 든다... 흐엉 너무 어렵다. 제로초님의
강의가 난도가 높다는 것을 알고 있었지만 그래도 너무 잘 알려주셔서 듣고 있었는데 약간 한계를 느꼈
달까... 패스트캠퍼스의 강의를 먼저 듣고 나서 다시 돌아와도 나쁘지 않을 것 같다.
어쨋든 오늘까지는 정말 열심히 들으며 공부하고 기록했다.
 
앞으로도 파이팅 !
 
 
 


 

  • 제로초 JavaScript 강의 - 로또 추첨기 순서도 그리기, 무작위로 공 뽑기, 공 정렬하기, 일정 시간 후에 실행하기, var과 let의 차이

 
 
<순서도 그리기>
 

로또 순서도

 
: 로또 추첨기의 순서도는 매우 간단하다고 하는데... 난 어렵다 흐엉
무작위로 공을 뽑는 방법은 앞의 강의에서 구현했기 때문에 순서도를 그림과 같이 그릴 수 있다.
 

긴장감을 위해 뽑은 공을 한 번에 화면에 보여 주는 것이 아니라 1초에 하나씩 보여 주겠다. 공이 7개이므로
총 7초 동안 보여 준다.
순서도는 간단하지만, 비동기 특성 때문에 실제로 구현하기는 어렵다. 로또 추첨기를 구현해 보면서
어떤 이유 때문에 어렵다고 느끼는지 알아보겠다.
 
 
 
<무작위로 공 뽑기>
 
: lotto.html 파일을 새로 만들어 다음과 같이 코딩한다.
 

<!DOCTYPE html>
<head>
  <meta charset="utf-8">
  <title>로또추첨기</title>
  <style>
    .ball {
      display: inline-block;
      border: 1px solid black;
      border-radius: 20px;
      width: 40px;
      height: 40px;
      line-height: 40px;
      font-size: 20px;
      text-align: center;
      margin-right: 20px;
    }
  </style>
</head>

<body>
<div id="result">추첨 결과는? </div>
<div id="bonus">보너스: </div>
<script>
</script>
</body>

 
숫자를 무작위로 뽑는 로직은 이미 앞 장에서 만들었다. 앞에서 미리 배웠던 무작위로 숫자뽑기와 달리
숫자가 적힌 공을 뽑는다고 생각하면 된다.
이번에는 전체 공 개수를 45개로 하고 이 중에서 보너스 공까지 총 7개를 뽑으면 된다. 마지막 공이 보너스 공이다.
다만, 앞 장과 차이를 두기 위해 공 45개를 전부 다 섞은 뒤에 7개의 공을 뽑겠다.
 

<script>
  const candidate = Array(45).fill().map((v, i) => i + 1);
</script>

 
1에서 45까지의 숫자를 추첨하는 코드다. 앞 장에서 배운 대로 for 문을 사용하지 않고 배열의 메서드를 사용해
뽑았다. 뽑은 값들은 candidate 변수에 저장했다.
 
이제 준비된 45개의 숫자를 섞어 보자. 빈 배열(shuffle) 하나를 두고 candidate 변수에서 하나씩 무작위로 값을
뽑아 옮기면 된다.
 

<script>
  const candidate = Array(45).fill().map((v, i) => i + 1);
  const shuffle = [];
  while (candidate.length > 0) {
    const random = Math.floor(Math.random() * candidate.length); // 무작위 인덱스 뽑기
    const spliceArray = candidate.splice(random, 1); // 뽑은 값은 배열에 들어 있음
    const value = spliceArray[0]; // 배열에 들어 있는 값을 꺼내어
    shuffle.push(value); // shuffle 배열에 넣기
  }
  console.log(shuffle);
</script>

 
이번에는 while 문을 사용했다. candidate 배열에서 무작위로 뽑은 숫자를 shuffle 배열로 하나씩 옮긴다. 
candidate 배열의 길이가 0이 될 때까지 이를 반복하므로 while 의 조건식(candidate.length > 0)을 앞의
코드처럼 작성한다.
 
이것이 피셔-예이츠 셔플(Fisher-Yates Shuffle)이라는 알고리즘(algorithm)다.
알고리즘이라는 단어를 들으면 흔히 복잡한 코드를 떠올리지만, 지금까지 우리가 작성한 코드도 모두 알고리즘이다.
그저 입사 시험 문제로 어려운 알고리즘을 주로 내서 알고리즘은 어렵다는 편견이 있을 뿐이다.
 
 
 
 
<공 정렬하기>
 
: 공 45개를 뽑았다. 현재 shuffle 안에 들어 있는 순서가 뽑은 순서이다.
이를 오름차순으로 정렬하려면 어떻게 해야 할까?
 
사람이라면 어떻게 정렬할지 생각해 보자. 전체 숫자를 쭉 훑어보면서 가장 작은 숫자를 하나 가져오고, 다시
전체를 쭉 훑어보다가 그다음 작은 숫자를 가져온다. 이런 식으로 숫자가 작은 순서대로 하나씩 가져오다 보면
모든 숫자가 정렬되는데, 이렇게 정렬하는 방식을 선택 정렬(selection sort) 알고리즘이라고 한다.
하지만 이 방식은 놀랍게도 가장 효율적인 방식이 아니다. 문제가 어느 정도 복잡해지면 보통 사람이 직관적으로
떠올리는 방식보다 더 좋은 해결법(더 효율적인 알고리즘)이 존재한다.
 
각종 정렬 방법을 소개하고 구현하기는 어려우므로 자바스크립트가 제공하는 배열의 정렬 메서드를 사용하겠다.
자바스크립트는 정렬을 위한 sort 메서드를 제공한다. 배열의 크기가 커질수록 정렬의 효율이 선택 정렬보다 훨씬
좋아진다.
 

const candidate = Array(45).fill().map((v, i) => i + 1);
const shuffle = [];
for (let i = candidate.length; i > 0; i--) {
  const random = Math.floor(Math.random() * i);
  const spliceArray = candidate.splice(random, 1);
  const value = spliceArray[0];
  shuffle.push(value);
}
console.log(shuffle);
const winBalls = shuffle.slice(0, 6).sort((a, b) => a - b);
const bonus = shuffle[6];
console.log(winBalls, bonus);

 
sort 메서드 안에 함수가 들어 있다. 이 함수에 적힌 규칙에 따라 배열이 정렬된다.
이런 함수를 비교 함수라고 한다.
 

(a, b) => a - b

 
비교 함수의 매개변수로 a와 b가 주어질 때 반환값에 따라 배열이 다르게 정렬된다.
현재 비교 함수의 반환값은 a - b이다. a - b가 0보다 크면 b, a 순서로 정렬되고, a - b가 0보다 작으면 a, b 순서대로
정렬된다. 0이면 순서가 유지된다.
 
a, b가 어떤 값인지 알아보려면 배열에서 아무 값이나 두 개를 뽑아보면 된다.
예를 들어, [1, 3, 16, 27, 44]라는 배열이 있을 때 44와 16을 뽑았다고 치면, 각각 a와 b가 돼서 a - b는 28로 0보다
크다. 그러면 16, 44로 정렬한다. 배열에서 뽑을 수 있는 모든 쌍에 이를 적용하면 배열의 값들이 오름차순으로
정렬된다.
 
 
 
 
<일정 시간 후에 실행하기>
 
: 이제 뽑은 공들을 화면에 표시하겠다. 긴장감을 위해 1초에 하나씩 뽑기로 했다.
자바스크립트에서는 setTimeout 함수로 지정한 시간 뒤에 코드가 실행되게 할 수 있다.
 
 

형식

setTimeout(() => {
  // 내용
}, 밀리초);

 
이때 setTimeout 안에 넣는 함수는 특정 작업(지정한 시간까지 기다리기) 이후에 추가로 실행되는 함수이므로 
콜백 함수로 볼 수 있다.
 
두 번째 인수가 밀리초 단위이므로 원하는 초에 1000을 곱해야 한다. 첫 번째 인수로 넣은 함수는 지정한 밀리초
후에 실행된다. 첫 번째 공을 1초 뒤에 뽑는다면 다음과 같다.
 

...
console.log(winBalls, bonus);
const $result = document.querySelector('#result');
setTimeout(() => {
  const $ball = document.createElement('div');
  $ball.className = 'ball';
  $ball.textContent = winBalls[0];
  $result.appendChild($ball);
}, 1000);

 
ball 태그는 기존 HTML에 없던 태그이므로 document.createElement 메서드로 새로 생성했다.
태그의 클래스를 'ball'로 지정해 style 태그 안에 들어 있는 CSS를 적용하고(자바스크립트에서 class는
예약어이므로 className으로 대신 표현한다), 태그의 내용물로는 winBalls 배열의 첫 번째 요소를 넣었다.
 
마지막으로, #result 태그 안에 ball 태그를 추가한다.
 
HTML을 실행하면 다음과 같은 화면이 뜬다.
무작위로 뽑으므로 브라우저를 새로고침할 때마다 공의 숫자가 달라진다.
 

공 하나를 뽑은 모습

 

NOTE 타이머의 시간은 정확한가?

아쉽게도 정확하지 않다. 자바스크립트는 기본적으로 한 번에 한 가지 일만 할 수 있다.

따라서 이미 많은 일을 하고 있다면 설정한 시간이 되어도 setTimeout에 지정된 작업이 수행되지 않는다.

기존에 하고 있던 일이 끝나야 setTimeout에 지정한 작업이 실행된다.

 

 

 

 

<타이머와 반복문 같이 사용하기>

 

: 하나의 공이 화면에 표시되는 것을 확인했으니 7개의 공을 모두 화면에 표시해 보겠다. 

setTimeout을 7번 사용해도 되지만, 그렇게 하면 코드가 매우 중복되므로 반복문과 함께 사용하는 게 좋다.

다만, 보너스 공은 화면에 표시하는 위치가 다르므로 어떻게 해야 할지 고민해 보자.

 

일반 공 6개를 먼저 화면에 표시하겠다.

 

...
console.log(winBalls, bonus);
const $result = document.querySelector('#result');
for (let i = 0; i < winBalls.length; i++) {
  setTimeout(() => {
    const $ball = document.createElement('div');
    $ball.className = 'ball';
    $ball.textContent = winBalls[i];
    $result.appendChild($ball);
  }, 1000 * (i + 1));
}

 

for 문으로 0부터 winBalls.length - 1(5)까지 반복한다. 1000 * (i + 1) 밀리초마다 공을 하나씩 생성해서 

winBalls[i] 숫자를 넣고 있다. 1초에는 winBalls[0], 2초에는 winBalls[1], 3초에는 winBalls[2] 공이 생성된다.

 

보너스 공은 7초 뒤에 표시하면 된다.

 

const $bonus = document.querySelector('#bonus');
setTimeout(() => {
  const $ball = document.createElement('div');
  $ball.className = 'ball';
  $ball.textContent = bonus;
  $bonus.appendChild($ball);
}, 7000);

 

딱 봐도 일반 공을 뽑는 코드와 중복된다. 중복되지 않는 것은 매개변수로 만들고 중복 되는 것은 함수로 뽑아내면

된다.

 

const $result = document.querySelector('#result');
function drawBall(number, $parent) {
  const $ball = document.createElement('div');
  $ball.className = 'ball';
  $ball.textContent = number;
  $parent.appendChild($ball);
}

for (let i = 0; i < winBalls.length; i++) {
  setTimeout(() => {
    drawBall(winBalls[i], $result);
  }, 1000 * (i + 1));
}

const $bonus = document.querySelector('#bonus');
setTimeout(() => {
  drawBall(bonus, $bonus);
}, 7000);

 
drawBall 함수를 만든 뒤, 달라지는 부분인 숫자와 부모 태그를 각각 number, $parent 매개변수로 만들었다.
중복을 제거하니 훨씬 더 깔끔해진 모습이다.
 

공을 추첨한 모습

 
 
 
<var과 let의 차이 이해하기>
 
: 앞에서 작성한 반복문 부분을 잠깐 let에서 var로 바꿔 보자.
 

for (var i = 0; i < winBalls.length; i++) {
  setTimeout(() => {
    drawBall(winBalls[i], $result);
  }, 1000 * (i + 1));
}

 
단순히 var로 바꿨을 뿐인데 결과가 완전히 달라진다.
 

var로 바꿨을 때 결과

 
 
보너스 공을 빼고 모든 공에 숫자가 뜨지 않는다. winballs[i]와 i를 콘솔로 출력하면 모두 undefined 6으로
나온다.
 

for (var i = 0; i < winBalls.length; i++) {
  setTimeout(() => {
    console.log(winBalls[i], i);
    drawBall(winBalls[i], $result);
  }, 1000 * (i + 1));
}

⑥ undefined 6

 
모든 i가 6으로 나오는 것이 신기할 것이다. 이것이 바로 var와 let의 결정적인 차이다. 어째서 이렇게 다른
결과가 나오는 걸까?
 
변수는 스코프(scope, 범위)라는 것을 가진다. var함수 스코프let블록 스코프를 가진다.
 
예제를 보면서 알아보자.
 

> function b() {
    var a = 1;
  }
  console.log(a);

  Uncaught ReferenceError: a is not defined

 
a를 콘솔로 출력하면 에러가 발생한다. a는 함수 안에 선언된 변수이므로 함수 바깥에서는 접근할 수 없다.
이렇듯 함수를 경계로 접근 여부가 달라지는 것을 함수 스코프라고 한다.
 
이번에는 if 문 안에 var를 넣어 보자.
 

> if (true) {
    var a = 1;
  }
  console.log(a);

  1

 
var는 함수 스코프(함수만 신경 씀)라서 if 문 안에 들어 있으면 바깥에서 접근할 수 있다. 그런데 let은 다르다.
 

> if (true) {
    let a = 1;
  }
  console.log(a);

  Uncaught ReferenceError: a is not defined

 
let의 경우에는 에러가 발생한다. 바로 let이 블록 스코프(블록을 신경 씀)라서 그렇습니다. 블록은 if 문, for 문, 
while 문, 함수에서 볼 수 있는 {}를 의미한다. 블록 바깥에서는 블록 안에 있는 let에 접근할 수 없다.
const도 let과 마찬가지로 블록 스코프를 가진다.
 
for 문도 보자.
 

> for (var i = 0; i < 5; i++) {}
  console.log(i);

  5

 
var는 블록과 관계가 없어서 문제없이 돌아간다. for 문이 끝났을 때 i가 5가 되어 있다는 점이 중요하다.
let은 에러가 발생한다. for 문 블록 바깥에서 접근했기 때문이다. 위치상으로는 let이 블록 바깥에 있지만, 
for 은 블록 안에 있는 것으로 친다.
 

> for (let i = 0; i < 5; i++) {}
  console.log(i);

  Uncaught ReferenceError: i is not defined

 
블록 스코프함수 스코프에 관해 간략히 알아봤다. 이 절 처음에 나온 반복문에서 let과 var를 사용한 결과가
다른 이유를 다시 살펴보자.
 

for (var i = 0; i < winBalls.length; i++) {
  setTimeout(() => {
    console.log(winBalls[i], i);
    drawBall(winBalls[i], $result);
  }, 1000 * (i + 1));
}

 
setTimeout의 콜백 함수(음영 부분) 안에 든 i와 바깥의 1000 * (i + 1)는 다른 시점에 실행된다.
 
1000 * (i + 1)는 반복문을 돌 때 실행되고, setTimeout의 콜백 함수는 지정한 시간 뒤에 호출된다. 그런데 반복문은
매우 빠른 속도로 돌아서 콜백 함수가 실행될 때는 이미 i가 6(winBalls.length가 6)이 되어 있다.
 
 

실행 순서

i가 0일 때 setTimeout(콜백0, 1000) 실행
i가 1일 때 setTimeout(콜백1, 2000) 실행
i가 2일 때 setTimeout(콜백2, 3000) 실행
i가 3일 때 setTimeout(콜백3, 4000) 실행
i가 4일 때 setTimeout(콜백4, 5000) 실행
i가 5일 때 setTimeout(콜백5, 6000) 실행
i가 6이 됨 
1초 후 콜백0 실행(i는 6) 
2초 후 콜백1 실행(i는 6)
3초 후 콜백2 실행(i는 6) 
4초 후 콜백3 실행(i는 6) 
5초 후 콜백4 실행(i는 6) 
6초 후 콜백5 실행(i는 6)

 
따라서 콜백 함수가 실행될 때 i를 콘솔로 출력하면 6이 나오게 된다. 그리고 winBalls는 인덱스가 5까지밖에
없으므로 winBalls[6]은 undefined가 된다.
 
그렇다면 왜 let을 쓸 때는 이러한 문제가 발생하지 않았을까?
 

for (let i = 0; i < winBalls.length; i++) {
  setTimeout(() => {
    console.log(winBalls[i], i);
    drawBall(winBalls[i], $result);
  }, 1000 * (i + 1));
}

 
for 문에서 쓰이는 let은 하나의 블록마다 i가 고정된다. 이것도 블록 스코프의 특성이라고 보면 된다.
따라서 setTimeout 콜백 함수 내부의 i도 setTimeout을 호출할 때의 i와 같은 값이 들어간다.
 
반복문과 var를 쓸 때 항상 스코프 관련 문제가 생기는 것은 아니다. setTimeout 같은 비동기 함수와 반복문, 
var를 만나면 이런 문제가 발생한다.
 
 
 
<마무리 요약>
 
1. 피셔-예이츠 셔플 알고리즘

 
: 숫자를 무작위로 섞는 방법이다. 먼저 무작위 인덱스를 하나 뽑은 후, 그에 해당되는 요소를 새로운 배열로
옮긴다. 이를 반복하다 보면 새 배열에 무작위로 섞인 숫자들이 들어간다.
 
 

2. sort

 
: 비교 함수에 적힌 내용대로 배열을 정렬하는 메서드다.
 
형식

배열.sort(비교 함수);

 
비교 함수는 다음과 같은 형태이다.
 
 
형식

(a, b) => 반환값

 
반환값이 0보다 크면 b, a 순으로 정렬되고, 0보다 작으면 a, b 순으로 정렬된다.
 
 
3. setTimeout
 
: 지정한 시간(밀리초) 뒤에 지정한 작업을 수행하는 타이머다.
 
 
형식

setTimeout(() => {
  // 내용
}, 밀리초);

 
 

4. 스코프

 
: var함수 스코프를, let블록 스코프를 가진다. 함수, if 문, for 문에서 접근 범위의 차이를 보인다.
또한, let을 사용할 때는 for 문 안에서 let 변수의 값이 고정되므로 var과는 실행결과가 달라진다.

반응형