로또추첨기

자바스크립트 실습 4일차

Posted by WaterMinCho on May 31, 2021 · 2842 lines

로또 추첨기(feat.비동기)

2021.04.30

소스코드: lotto.html / lotto-self.html


플로우

알고리즘

배운 개념

1. 비동기란?
2. map, slice
 - map
3. 피셔-예이츠 셔플 알고리즘
4. sort()
5. setTimeout()
6. var와 let의 차이(feat.클로저)
 - 변수
 - 실습파일


  • 비동기란?

    • 코딩한 순서와 다르게 동작하는 코드.(ex.EventListener(), setTimeout()…)
    • 추가적인 내용은 아래 var과 let의 차이에 설명이 있다.

  • map, slice

    • 둘 다 Array하위 메소드이며, 원본은 변하지 않는다.
    • 하지만 매칭해보면 원본이랑은 다르다.(복사 개념이기 때문)
    • slice는(자르기 시작할 인덱스, 끝 인덱스) 다만, 끝 인덱스는 자르기 대상에 포함하지 않는다.

      • 인덱스에 -값이 들어가면 뒤에서부터 읽어들인다.
      • splice는 원본이 변한다.(선택하여 꺼낸다.)
    • map()
    • array.map(callbackFunction(currentValue, index, array),thisArg)

      • map()함수는 모든 배열의 값이 Function을 실행하는 메소드다!
      • callbackFunction을 실행한 결과를 가지고 새로운 배열을 만들 때 사용한다.
      • callbackFunction, thisArg 총 두개의 매개변수가 있고, callbackFunctioncurrentValue,index,array 3개의 매개변수를 갖는다.

        • currentValue: 배열 내 현재 값.
        • index: 배열 내 현재 값의 인덱스.
        • array: 현재 배열
        • thisArg: callbackFunction 내에서 this로 사용될 값.
      • const days = ["Mon", "Tue", "Wed", "Thus", "Fri"];
        

        위와 같은 변수가 있다고 하자.
        여기서 days.map(…)을 적용한다는 것은 `days`에 있는 모든 요소에 `Function`을 실행하고 `Function`에서 나오는 값을 저장해서 새로운 배열로 만든다는 것이다. (그러므로 원본은 변하지 않는다.)

      1. 간단하게 square root (제곱근)을 구해보자

        const numbers = [4, 9, 16, 25, 36];
        const result = numbers.map(Math.sqrt);
        console.log(result);
        
        result[(2, 3, 4, 5, 6)];
        
      2. 이번에는 기존 배열에 값의 x2를 한 배열을 생성해 보자.
        다음 세 개는 결과가 같다.

        const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9];
        const newNumbers = numbers.map((number) => number * 2);
        console.log(newNumbers);
        
        result[(2, 4, 6, 8, 10, 12, 14, 16, 18)];
        
        const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9];
        const newNumbers = numbers.map(function (number) {
          return number * 2; // 이렇게 해도 결과는 위와 같다.
        });
        console.log(newNumbers);
        
        result[(2, 4, 6, 8, 10, 12, 14, 16, 18)];
        
        const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9];
        function multiplyTwo(number) {
          return number * 2;
        }
        const newNumbers = numbers.map(multiplyTwo);
        console.log(newNumbers); // 이렇게 해도 결과가 같다.
        
        result[(2, 4, 6, 8, 10, 12, 14, 16, 18)];
        

        위의 결과들은 다 같다.
        numbers에 곱하기 2한 값이 newNumbers에 똑같이 나타난다.
        map 함수는 기존의 배열을 callbackFunction에 의해 새 배열을 만드는 함수이다. 그러니 기존의 배열이 변하지는 않는다.

      3. map 함수는 filter함수와 같이 Object(객체)타입도 컨트롤 할 수도 있다.

        const students = [
          { id: 1, name: "james" },
          { id: 2, name: "tim" },
          { id: 3, name: "john" },
          { id: 4, name: "brian" },
        ];
        

        이러한 Object(객체)타입의 변수가 있다.
        여기서 이름만 추출하고 싶으면 Array map을 이용해서 쉽게 추출할 수 있다.

        const students = [
          { id: 1, name: "james" },
          { id: 2, name: "tim" },
          { id: 3, name: "john" },
          { id: 4, name: "brian" },
        ];
        const names = students.map((student) => student.name);
        console.log(names);
        
        result[("james", "tim", "john", "brian")];
        

        result를 보면 이름만 추출된 것을 볼 수 있다.
        여기서 student.name을 student.id로 바꾸면 id 값만 추출된다.

        const students = [
          { id: 1, name: "james" },
          { id: 2, name: "tim" },
          { id: 3, name: "john" },
          { id: 4, name: "brian" },
        ];
        var names = students.map((student) => student.id);
        console.log(names);
        
        result[(1, 2, 3, 4)];
        

        결과를 보면 id 값만 추출된 것을 볼 수 있다.

      4. 또다른 Object(객체)를 보자.

        const testJson = [
          {name : "이건", salary : 50000000},
          {name : "홍길동", salary : 1000000},
          {name : "임신구", salary : 3000000},
          {name : "이승룡", salary : 2000000}
          ];
          const newJson = testJson.map(function(element, index){
            console.log(element);
            const returnObj = {};
            returnObj[element.name] = element.salary;
            return returnObj;
            });
            console.log("newObj");
            console.log(newJson);
        
        
            result
            1. console.log(element);
            { name: '이건', salary: 50000000 }
            { name: '홍길동', salary: 1000000 }
            { name: '임신구', salary: 3000000 }
            { name: '이승룡', salary: 2000000 }
            2. console.log(newJson);
            [{ '이건': 50000000 },{ '홍길동': 1000000 },{ '임신구': 3000000 },{ '이승룡': 2000000 }]
        
      5. 이번에는 Array를 reverse.
        배열 값에 곱하기 2한 값들을 reverse한다.

        ```javascript
        const numbers = [1,2,3,4,5,6];
        const numbersReverse  = numbers.map(number => number *2).reverse();
        console.log(numbersReverse);
        
        result
        [ 12, 10, 8, 6, 4, 2 ]
        ```
        
      6. Array안에 Array가 있는 경우

        const numbers = [
          [1, 2, 3],
          [4, 5, 6],
          [7, 8, 9],
        ]; //array안에 array가 있는 경우
        const newNumbers = numbers.map((array) =>
          array.map((number) => number * 2)
        );
        console.log(newNumbers);
        
        result[([2, 4, 6], [8, 10, 12], [14, 16, 18])];
        

  • 피셔-예이츠 셔플 알고리즘

    while (candidate.length > 0) {
      //무작위 인덱스 뽑기.
      const random = Math.floor(Math.random() * candidate.length);
      //무작위 인덱스로 뽑은 값은 spliceArray배열에 들어있다.
      const spliceArray = candidate.splice(random, 1);
      //배열에 들어있는 값을 꺼낸다.
      const value = spliceArray[0];
      //shuffle배열에 삽입.
      shuffled.push(value);
    }
    

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


  • sort()

    • sort의 (a,b)가 있으며 원본을 바꾼다.
      array.sort(function(a,b){a-b};); --> array.sort((a,b)=>{a-b};)
      로 하면 결과값이 오름차순[1,2,3,4,5,6,7,8,9]가 나온다.
      반대로array.sort((a,b)=>{b-a};)</code>로 하면
      내림차순[9,8,7,6,5,4,3,2,1]이 나온다.
    • 만약 원본을 바꾸는 메서드가 있는 경우 대처법은 array.slice().sort((a,b)=>{a-b})
      식으로 slice()를 중간에 적용시키면 원본은 그대로 냅두고 복사형식으로 처리할 수 있다.

  • setTimeout()

    setTimeout(() => {
      //실행문
    }, millisecond);
    
    • 타이머의 시간은 정확한가?
      • 정확하지 않다. 자바스크립트는 기본적으로 한 번에 한 가지 일만 할 수 있다. 따라서 이미 많은 일을 하고 있다면 설정한 시간이 되어도 setTimeout에 지정된 작업이 수행되지 않는다. 기존에 하고 있던 일이 끝나야 setTimeout에 지정한 작업이 실행된다.

  • var와 let의 차이(feat.클로저)

    내가 면접 때 오지게 절었던 부분 중 하나

    • 실습파일(lotto.html)의 for문에서 let i = 0 을 var i = 0으로 바꾸면 undefined 6이 나온다.즉, 모든 i가 6으로 나온다는 것이다.

    • 변수

      • 변수는 스코프라는 것을 가진다. var는 함수 스코프를 가지고 let은 블록 스코프를 가진다.
      • function b(){
          var a = 1;
        }
        console.log(a);
        
        result
        Uncaught ReferenceError: a is not defined
        
        • 위와 같이 함수b 내부에 var a를 정의한 후 함수 바깥에서 변수 a를 호출했더니 에러가 발생했다. 이유인 즉슨 var는 함수 내에서만 동작하는 지역변수이기 때문이다.
        • 이와 같이 함수를 경계로 접근 여부가 달라지는 것함수 스코프라고 한다.
      • if (true) {
          var a = 1;
        }
        console.log(a);
        
        result;
        1;
        
      • 함수 범위에 없고 조건문 내에 있기 때문에 a값이 그대로 출력되는 것을 볼 수 있다. 즉, 함수만 신경쓴다는 것인데 let은 조금 다르다.

      • if(true){
          let a = 1;
        }
        console.log(a);
        
        result
        Uncaught ReferenceError: a is not defined
        
        • 이것봐라? 잘 보면 {}(블록)범위 내에서만 살아숨쉬는 앙증맞은 let이라는 것을 확인할 수 있었다.
        • 그럼 if, switch, for, while(?얘는 case by case) 등의 함수가 아닌 블록 내부에서만 접근이 허용된다는 것을 유추할 수 있다.
    • 실습파일(lotto.html)

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

      위의 코드 중

      () => {
        console.log(winBalls[i], i);
        drawBall(winBalls[i], $result);
      };
      

      이 부분은 실제 코드와 다른 시점에 실행된다. 위 코드가 실행이 될 때는 i값은 이미 6이 되어있다.

      for루프를 모두 돌고 i값이 6이 된다
      -> 1초가 된다
      -> winBalls[6]이 나온다
      -> winBalls[6]은 winBalls배열의 범위(5)를 넘었기에 undefined로 나온다
      -> log값이 'undefined 6'으로 출력된다 라는 일련의 과정으로 이해할 수 있다`.
      나중에 커서 보고 이해 못하는 나 또는 다른 분들에겐 친절하게 루프로 보여주겠다.

      i가 6이 됨
      1초 후 콜백 0실행(i=6)
      2초 후 콜백 0실행(i=6)
      3초 후 콜백 0실행(i=6)
      4초 후 콜백 0실행(i=6)
      5초 후 콜백 0실행(i=6)
      6초 후 콜백 0실행(i=6)

    • 그렇다면 왜 let을 쓸 때는 이러한 문제가 발생하지 았았는가?
      아래를 보자.
    for (let i = 0; i < winBalls.length; i++) {
      setTimeout(() => {
        console.log(winBalls[i], i);
        drawBall(winBalls[i], $result);
      }, (i + 1) * 1000);
    }
    

    for문에서 쓰이는 let은 하나의 블록마다 i가 고정된다. 이것도 블록 스코프의 특성이라고 보면 된다.
    따라서 setTimeout 콜백 함수 내부의 isetTimeout을 호출할 떄의 i와 같은 값이 들어간다.

    “아니 그럼 var쓰던 사람들은 저렇게 코드를 짤 수 없는겁니까?”

    그래서 나온게(?) 클로저다. 아래를 보자.

    for (var i = 0; i < winBalls.length; i++) {
      //함수를 소괄호로 감싼다 = 그 자리에서 바로 호출한다.
      (function (j) {
        //j는 매개변수이고 함수블록 안에 갇혀있다.
        setTimeout(() => {
          console.log(winBalls[j], j); // 함수의 인자j는 for문의 var i를 가리킨다.
          drawBall(winBalls[j], $result);
        }, (i + 1) * 1000);
      })(i);
    }
    

    클로저함수와 함수 바깥에 있는 변수와의 관계다.
    함수 바깥에 있는 변수를 함수 안에 있는 변수로 바꿔주고 내부 함수의 변수로써 사용하도록 억지 세팅해준거다 그냥 let쓰자…


그래도 난 개발지식이 태아수준이라 현상 자체는 알아야 한다.
함수랑 함수 자체를 바깥에 있는 변수들을 통틀어 클로저라 칭하고 그 클로저variable이 만나면 위의 문제가 발생할 수 있다. 근데 모든 케이스가 그렇지 않고 조건이 껴있다. 바로 비동기다.
함수 스코프를 지닌 variable비동기함수가 만나면 클로저문제가 발생한다. 그렇다고 무조건 let을 쓰는것은 좋지 않다 왜냐하면 레거시코드에서 var을 쓰는 경우가 있기 때문에 해당 현상에 대해서 충분히 이해하고 설명할 수 있어야 한다.