티스토리 뷰

* jQuery 스타일 적용

// util.js
export const $ = selector => document.querySelector(selector);
export const $$ = selector => document.querySelectorAll(selector);

// app.js
const $carNameInput = $('#car-name-input');

 document.querySelector로 DOM을 조작하는 코드를 작성하는 것보다 jQuery 스타일로 코드를 작성하는 편이 훨씬 보기에 깔끔하고 명확했다. 위 코드는 Jbee가 다른 크루들에게 리뷰해준 내용을 참고했다.

 

* 게임 종료 후 승자를 찾는 알고리즘

getWinners() {
  let maxDistance = -1;

  for (let i = 0; i < this.cars.length; i++) {
    if (this.cars[i].distance > maxDistance) {
      maxDistance = this.cars[i].distance;
    }
  }

  return this.cars.filter((car) => car.distance === maxDistance);
}

 나와 체프가 작성한 코드는 반복문을 돌면서 가장 많이 이동한 거리를 찾은 뒤, 필터를 사용하여 자동차의 이동거리가 최대 이동거리와 같은 경우를 리턴한다. 이는 반복문을 두 번 사용한 것과 같다.

export const getWinners = (cars) => {
  let winners = [];
  let maxForwardCount = -1;

  cars.forEach((car) => {
    if (car.forwardCount > maxForwardCount) {
      winners = [car.name];
      maxForwardCount = car.forwardCount;
    } else if (car.forwardCount === maxForwardCount) {
      winners.push(car.name);
    }
  });
  return winners.join(', ');
};

 반면에 유조와 하루가 작성한 코드에서는 for문을 한번만 돌아도 승자를 리턴할 수 있도록 짜여졌다. 반복문을 돌면서 기존의 최대 이동거리보다 더 많은 이동거리를 가진 자동차가 있는 경우에 승자 배열을 초기화하고 최대 이동거리와 같은 경우에 승자 배열에 자동차를 추가하는 식이다. 이 때문에 반복문을 두 번 사용할 필요가 없어 더 효율적이다!

 

 

Cypress

* alert 창이 정상적으로 뜨는지 테스트하기

it('자동차 이름이 비어있는 경우 경고창을 띄운다.', () => {
    cy.get('#car-names-submit').click();
    cy.on('window:alert', (txt) => {
      expect(txt).to.contains(alertConstants.INVALID_CAR_NAME);
    });
  });

 본인이 작성한 테스트 코드는 alert를 정확히 테스트하지 못했다. 예를 들어, 자동차 입력 칸에 아무 내용도 없이 확인 버튼이 클릭 됐을 때, 의도한대로 프로그램이 동작하기 위해선 '자동차 이름을 1~5 글자 이내로 입력해주세요!'와 같은 경고창을 띄워야한다. 그러나 위의 코드는 아래의 코드처럼 버튼을 클릭하는 과정을 생략해도 정상적으로 테스트를 통과하게 된다. 그 이유는 alert가 뜨는지 여부를 체크하는 것이 아니라 alert가 떴을 때, alert message가 내가 원하는 대로 출력되는지를 확인하는 코드이기 때문이다. 즉, alert message가 기대하는 것과 다를 때만 에러를 알려준다. alert가 안 뜬 경우는 무조건 통과한다...

it('자동차 이름이 비어있는 경우 경고창을 띄운다.', () => {
    cy.on('window:alert', (txt) => {
      expect(txt).to.contains(alertConstants.INVALID_CAR_NAME);
    });
  });

버튼을 클릭하지 않아도 에러 없이 테스트를 통과

 

 따라서 alert가 정상적으로 뜨는지와 동시에 alert message가 내가 원하는 내용인지 테스트하기 위해선 아래와 같이 코드를 작성해야한다. 아래의 코드는 유조와 하루의 코드를 참고했다.

it('자동차 이름이 비어있는 경우 경고창을 띄운다.', () => {
    const alertStub = cy.stub();

    cy.on('window:alert', alertStub);
    cy.get('#car-names-submit')
      .click()
      .then(() => {
        expect(alertStub.getCall(0)).to.be.calledWith(alertConstants.INVALID_CAR_NAME);
      });
  });

 

 아래와 같이 코드를 작성하면 alert가 뜨지 않는 경우에도 에러를 검출할 수 있다.

it('자동차 이름이 비어있는 경우 경고창을 띄운다.', () => {
    const alertStub = cy.stub();

    cy.on('window:alert', alertStub).then(() => {
      expect(alertStub.getCall(0)).to.be.calledWith(alertConstants.INVALID_CAR_NAME);
    });
  });

버튼을 클릭하지 않으면 에러를 띄움

 

동기화, 비동기화

* 콜 스택, web api, 태스크 큐 사이의 관계에 대해 잘 정리한 영상

www.youtube.com/watch?v=8aGhZQkoFbQ&list=PLtwKaVUIYnVXNnjq8RtRWDhagMrFNKcO0&index=1&t=977s&ab_channel=JSConf

 

* Promise, async, await

 자바스크립트는 싱글 스레드로 동작하기 때문에 원래대로라면 동기적으로 동작하는 것이 맞다. 그러나, 실제로 웹에서 자바스크립트는 비동기적으로 동작한다. 어떻게 이것이 가능한 것일까? 이에 대한 대답은 위에 있는 영상을 보면 확인할 수 있다. 웹 api에서 비동기를 지원하기 때문에 우리는 자바스크립트에서 시간이 걸리는 일을 처리하는 동안에도 다른 일들을 수행할 수 있다. 그러나 때로는 동기적으로 일을 처리해야하는 경우가 생긴다.

 

 예를 들어, 게임 결과를 2초 뒤에 출력하는 웹페이지를 만든다고 가정해보자.

startRacing() {
    for (let i = 0; i < this.model.racingCount; i++) {
      setTimeout(() => {}, 1000); // 1초 동안 대기
      const movedCars = this.getMovedCars();
      this.moveCars(movedCars);
      this.view.renderRacingRoundResult(movedCars);
    }
    this.view.removeSpinner();
    this.showRacingResult();
  }

showRacingResult() {
  this.view.show(this.$resultContainer);
  const winners = this.model.getWinners();
  this.view.renderRacingResult(winners);
}

startRacing();

위와 같이 코드를 작성하면 setTimeout이 정상적으로 동작을 하지 않는 것처럼 보인다. 예상대로라면 반복문을 돌 때마다 1초씩 대기한 후 자동차를 움직여야한다. 그리고 각 라운드의 결과가 하나씩 출력되야 한다. 그러나 예상과 달리 결과가 한번에 출력된다.

 그 이유는 자바스크립트가 비동기적으로 처리됐기 때문이다. setTimeout을 동기적으로 처리하라는 지시가 없었기 때문에 setTimeout은 호출되자마자 콜스택에서 사라지고 web api에서 처리된다. 이 때문에 밑의 코드들이 콜스택에 올라갈 수 있고, 우리의 예상과는 다른 결과를 얻게 된다.

 

 

 그렇다면 우리는 setTimeout을 동기적으로 처리하라는 지시를 내려줘야한다.

function sleep(ms) {
  return new Promise((resolve, _) => {
    setTimeout(() => {
      resolve();
    }, ms);
  });
}

async startRacing() {
    for (let i = 0; i < this.model.racingCount; i++) {
      await sleep(1000);
      const movedCars = this.getMovedCars();
      this.moveCars(movedCars);
      this.view.renderRacingRoundResult(movedCars);
    }
    this.view.removeSpinner();
    this.showRacingResult();
  }

showRacingResult() {
  this.view.show(this.$resultContainer);
  const winners = this.model.getWinners();
  this.view.renderRacingResult(winners);
}

startRacing();

 

 sleep이라는 새로운 함수를 작성하였다. sleep은 Promise를 생성하고 리턴하는데 Promise는 다음과 같다.

 

 

 Promise에서 비동기 연산이 정상적으로 처리되면 resolve를 반환한다. resolve가 반환된 프로미스의 경우 .then이나 await을 사용하여 동기적으로 처리가 가능하도록 만들 수 있다. 반면 중간에 오류가 발생한 경우에는 reject를 반환한다. 이는 catch문을 통해 처리할 수 있다.

 

 asyncawait은 비동기 처리를 조금 더 깔끔하게 할 수 있는 es6 문법이다. 기본적으로 await은 프로미스 앞에서만 사용할 수 있다. 또한, await을 사용하기 위해서는 await을 품고 있는 함수가 async 함수여야 한다. await은 비동기 연산의 처리가 완료된 경우에만 다음 코드를 진행할 수 있도록 한다.

 

 따라서 위의 코드를 다시 해석해보면 Sleep함수는 setTimeout 메서드가 정상적으로 수행된 경우 fullfilled 상태의 프로미스를 반환한다. startRacing 앞에 async가 붙은 이유는 await을 사용하기 위함이다. Sleep이라는 함수는 동기적으로 처리돼야하므로 앞에 await을 붙인다.

 

 그러면 우리가 예상하던 결과를 얻을 수 있다.

 

 그 외

본인의 질문
리뷰어 자프의 답변

 

Promise.all(), Array.Prototype.some(), 옵저버 패턴, 오버엔지니어링 등등 새로운 용어와 메소드에 대해서도 배울 수 있었다.

 

 

총평

 누군가에게 내 코드를 보여주는 것이 처음이라 조금은 긴장이 됐다. 그래도 리뷰어분께서 친절하게 내가 궁금했던 점이나 몰랐던 부분을 설명해주셔서 많은 도움이 됐다. 또한, 다른 사람의 코드를 참고하는 것도 아주 큰 도움이 됐다. 모두 목표하는 기능은 동일하지만 그 속은 꽤나 달랐다. 그래서 더 많은 인사이트를 얻을 수 있었다. 

 

 페어프로그래밍을 하기 전에는 MVC 패턴으로 코드를 짜본 적이 없었다. 페어인 체프의 도움이 없었다면 많이 헤맸을 것 같다. 같이 하는 사람이 있으니까 집중도와 책임감이 높아져서 더 열심히 할 수 있었다.

 

 Cypress를 사용하여 테스트를 작성해보는 것도 신선한 경험이었다. 이전까지는 테스트의 필요성을 못 느꼈는데 이제는 테스트가 왜 중요한지 확실히 이해했다.

 

 앞으로 남은 기간도 화이팅해서 더욱 더 성장해나가고 싶다. 초심을 잃지말고 꾸준히 노력하자.