⛏️로또 게임 Promise 객체 적용기 - 삽질 일지

김영우·2022년 11월 14일
0
post-thumbnail

당신이 프론트엔드를 공부중이라면 Promise 객체를 들어봤거나 언젠가 듣게 될 것이다. 오늘은 모던 자바스크립트를 혼자 공부하다 로또 게임에 이 Promise 객체를 적용해본 삽질을 기록해보고자 한다. 먼저 Promise가 뭔지부터 알아보자.


🧐 Promise가 뭔데?

promise의 사전적 정의를 찾아보았다.

우리가 평소에 알던 그 단어가 맞다. 바로 약속을 의미하는 단어이다.

약속을 또 사전에서 찾아보았다.

약속이란 미래의 어떤 행동에 대해 미리 정하는 것을 의미한다. Promise 객체는 그 단어 의미에서도 알 수 있듯이 미래에 있을 어떤 행동을 지정해주는 역할을 한다.


⌚ 언제 쓰는건데?

Promise 객체는 미래의 행동을 지정해준다고 했다. 따라서 Promise 객체는 지금 당장 수행할 것이 아니라 이후에 있을 일을 지정해줄 때 사용한다. 프론트엔드에서 미래의 행동을 지정해 줄 일에는 어떤 경우가 있을까?

대표적으로 api를 통해 데이터를 받아오는 경우가 있다.

let data = await fetch('데이터를 받아올 url');
console.log(data);

위 코드를 실행시키면 콘솔에 undefined가 출력됨을 확인할 수 있을 것이다. 코드가 실행되면 먼저 data라는 변수가 생성되고, fetch가 데이터를 받아올 url에 요청을 넣을 것이다.

요청을 넣는 동시에 데이터를 받아오면 좋겠지만 실제론 그렇지 않을 것이다. 그렇기 때문에 data변수는 생겨난 이후 어떠한 값도 저장되지 않았기에 undefined를 출력하게 될 것이다.

평소에 fetch를 써봤다면 아마도 위의 방식보다는 다음과 같이 사용해왔을 것이다.

let data;

fetch('데이터를 받아올 url')
.then((response) => response.json())
.then((json) => { data = json })
.catch((error)=>{ alert(error) });

console.log(data);

우리는 평소에 위와 같은 방식으로 코드를 실행했을 때 받아와지지 않은 데이터에 대해 미리 처리 방식을 지정해주었다.

이때 Promise 객체가 사용된 것이다.


😅 그래서 어떻게 쓰는데?

기본적으로 Promise는 실행자(executor)를 인자로 받는다. 그리고 그 실행자는 resolvereject를 각각 첫번째, 두번째 인자로 넘겨 받는다. 말로는 어려우니 코드를 보자.

new Promise((resolve, reject) => {
  if( 로직 성공! ) resolve("성공했지롱");
  reject("실패했당...");
});

Promise 속의

(resolve,reject) => {...}

이 부분이 실행자이고, 인자로 넘겨받는 두 친구들은 기본적으로 주어지는 친구들이니 굳이 우리가 구현하지 않아도 된다.

위 첫번째 예시에서 보다시피 Promise의 내부 로직이 성공하면 resolve를 통해 데이터를 넘겨주고, 실패했다면 reject를 통해 에러 메시지를 전달하는 방식이다. 이후 로또 게임에서의 적용 방식을 보며 더 자세히 알아보자.


🧭 로또 게임 어디에서 썼을까?!

우테코 5기 프리코스에서는 공통적으로 사용하는 MissionUtils라는 라이브러리가 있다. 랜덤값과 사용자 입출력을 다루는 메서드들이 포함돼있는 라이브러리이다.

내가 로또 게임에 Promise를 적용한 부분은 바로 이 MissionUtilsConsole.readLine 메서드를 사용하는 부분이다.


이 글을 읽고있는 우테코 5기 지원자 분들은 MissionUtils를 사용해 로또 게임을 어떻게 짤까 고민하면서 한번쯤은 나와 같은 고민을 했을 것이다. 그 고민은 바로!

🧐 순서를 어떻게 주입하지..?

였을 것이다. 아닐수도 있지만 아마도 많은 분들이 공감할 것이라 생각한다.

으잉? 저게 뭔 소리여? 하실 분들이 꽤 있을 것 같다. 아마 뒷 내용을 들어보면 아~~ 저 얘기였구나 하실거다.

아 그리고 아까 미쳐 이야기하지 못했는데 이 글이 삽질 일지인 이유는 구현은 성공했지만 테스트를 성공하지 못해서 롤백했기 때문이다.

(💪💪테스트 코드 고쳐서 언젠간 깨고만다!!)

이제 삽질 과정을 한번 이야기해보겠다.


⛏️ 삽질의 시작

이모지에 삽이 없어서 곡괭이로 대체했다.

MissionUtils.Console.readLine을 처음 사용한 것은 2주차 미션 숫자야구 게임을 진행할 때였다. 그때도 느꼈지만 이번에도 똑같이 느낀 것이 있다. 코드를 보자.

const { Console } = required('@woowacourse/mission-utils');

class whatDoYouWant{
  wish;
  
  constructor(){};
  
  question(){
    while(this.wish !== "우아한 테크코스 합격!"){
      Console.readLine("올해 이루고 싶은 소망은?!",(input) => {
        this.wish = input;
      })
    }
    
    Console.print(this.wish);
  }
}

위 예시의 question 메서드를 실행시키면 어떤 결과가 예상되는가? 아마도 코드의 흐름상 유저의 입력이 "우아한 테크코스 합격!"과 일치한다면 while의 반복이 종료되고 콘솔에 해당 문구가 출력될 것이라 생각할 것이다.

하지만 실제 결과는 그저 무한 반복이다. 콘솔을 확인해보면 빠른 속도로 깜빡이며 "올해 이루고 싶은 소망은?!"이 반복적으로 출력되는 모습을 볼 수 있을 것이다. 위의 예시에서는 사용자의 입력을 기다려주지 않는다.

이 때문에 "사용자의 입력이 발생한다 => 다음 로직이 실행된다"의 순서를 어떻게 주입할지 고민을 많이 했다. 왜 이런 일이 발생하는 것일까?

이전 포스팅에서 테스트 코드를 분석하며 이 Console.readLine 또한 분석해봤었다. 분석해본 결과 Console.readLine 메서드는 콜백함수 내의 로직을 실행할 때 사용자의 입력을 기다려준다는 것을 알 수 있었다.

따라서 사용자의 입력을 기다렸다가 다음 로직을 실행하고 싶다면 Console.readLine 메서드 내부에 다음 로직 부분을 넣어주어야 한다. 그런데 로또게임에서는 입력을 받아야 하는 부분이 세 부분이나 존재한다.

Console.readLine("1번째 입력",(input)=>{
  firstLogic(input);
  
  Console.readLine("2번째 입력",(input)=>{
    secondLogic(input);
    
    Console.readLine("3번째 입력",()=>{
      thirdLogic(input);
      
      //...?
    });
  });
});

🥶 잘못했다간 위의 코드처럼 정말 보기 싫은 코드가 만들어질 수도 있다.


그런데 이 부분 뭔가 익숙하다. 기다렸다가 실행한다? 이는 곧 미래의 행동에 대해 코드를 작성하는 것과 같다. 앞에서 설명했던 Promise 객체가 등장할 순간이다.


⛏️ 어떻게 적용했을까?

나는 Console.readLine이 그 콜백함수 내부에서만이 아니라 그냥 실행만 하면 사용자의 입력을 기다려줬으면 했다. 그래서 다음과 같은 함수를 만들었다.

getUserInput(guideString,validate){
  return new Promise((resolve,reject)=>{
    Console.readLine(guideString,(input)=>{
      validate(input);
      
      resolve(input);
    })
  });
}

함수를 하나하나 뜯어보자.

일단 getUserInput은 이름에서도 알수 있듯이 사용자의 입력을 다루는 함수이다. 이 함수는 인자로 guideString, validate, additionalParameter 이렇게 세 친구를 넘겨받는다. 각각의 역할은 다음과 같다.

  1. guideString : 콘솔을 통해 출력되는 질문 문자열
  2. validate : 입력값이 유효하지 않으면 오류를 발생시키는 오류 검출 기능을 수행하는 함수

이러한 인자를 넘겨받은 getUserInputPromise를 반환한다. 이 Promise 내부에서 Console.readLine을 실행한 후 validate에서 오류가 발생하지 않았다면 resolve를 통해 입력값을 반환한다. 이를 실행하면 어떻게 될까?

const { Console } = required('@woowacourse/mission-utils');

class whatDoYouWant{
  wish;
  
  constructor(){};
  
  async question(){
    while(this.wish !== "우아한 테크코스 합격!"){
      await getUserInput("올해 이루고 싶은 소망은?!",validate)
        .then((input) => {
        	this.wish = input;
      	})
      	.catch((error)=>{ throw new Error(error) };
    }
    
    Console.print(this.wish);
  }
}

아까 보여주었던 예시에서 사용자의 입력을 getUserInput으로 바꾼 모습이다. 이제 question 메서드를 실행하면 콘솔은 사용자의 입력을 기다려줄 것이다. async, await를 통해 Promise 객체가 resolve를 호출하든 reject를 호출하든 둘 중 하나를 수행할 때까지 기다리기 때문이다.

실제로 이 방법을 이용해서 구현을 성공적으로 마쳤다. 하지만 테스트코드는 통과하지 못했다...

실제로 테스트를 돌려보면 콘솔의 출력은 잘 수행함을 알 수 있다.

하지만 MissionUtils.Console.print의 호출 수가 0임을 확인할 수 있다. 테스트를 진행하는 동안 전혀 호출되지 않았다는 것이다.


😭😭 왜 실패했을까?

이유는 간단하다. 비동기적인 방식을 사용해 구현했기 때문이다. 테스트 코드를 보면 Console.readLine의 콜백 함수에 미리 지정된 값을 넣어 모의 함수를 실행시키는 것을 알 수 있다. (이전 포스팅 참조)

주어진 테스트는 완전히 동기적으로 작동하는 프로그램을 위한 테스트이다. 그래서 사용자 임의대로 값을 지정해 그 값을 모의 함수에 전달해 실행시키는 방식으로 실행된다.

하지만 내가 작성한 코드는 비동기적으로 실행되기 때문에 특별한 방식을 사용해주어야 한다.

그냥 호출되는 것이 아닌 비동기적으로 수행된 코드의 결과에 의해 호출되는 상황을 테스트 해야하는 것이다.

아직은 테스트 코드에 대한 공부가 미숙하여 내가 짠 코드를 통과시키는 테스트 코드를 작성하지 못하지만, 4주차 미션을 마치게 되면 jest를 더 공부해서 내가 짠 코드가 통과하는 테스트 코드를 작성해볼 생각이다.


👋 마무리

혼자 공부해보며 많은 시행착오를 겪은 탓에 할 말이 많아져 글이 엄청 길어지게 됐다. 하지만 공부해본 내용을 실제로 적용해보고자 했고, 그 과정 속에서 배운점이 정말 많았기에 이 경험을 있는 그대로 기록하고, 공유하고자 했다. 함께 프리코스를 진행하고 있는 많은 지원자 분들도 이 글을 통해 Promise에 대해 다시한번 상기해보거나 새로 공부해보는 계기가 되었으면 한다.

글을 적다가 너무 길어질 것 같아 줄이고 줄인 탓에 Promise에 대해 자세히 다루지 못했는데 혹시라도 어디서 공부해야될 지 모르겠다면 이 링크를 참고하기 바란다. 그리고 혹시 롤백하기 전의 코드 전체를 보고 싶다면 우테코 5기 프리코스 3주차 미션 - promise study 이 브렌치의 src/utils/GameUtils.jsgetUserInput 메서드를 참고하기 바란다.

profile
불편한 일들을 개발로 풀어내고 싶은 프론트엔드 개발자입니다!

0개의 댓글