콘솔 기반 로또 게임을 웹 UI로 변경하기

정균·2023년 4월 19일
3

우아한테크코스

목록 보기
5/15
post-thumbnail

개요

레벨1의 두 번째 미션인 로또 미션에서 중점을 뒀던 키워드는 도메인과 UI의 로직 분리콘솔 UI에서 웹 UI로 마이그레이션이다.

이번 미션에서 step1은 콘솔 UI로 구현하고, step2에서는 해당 애플리케이션을 웹 UI로 마이그레이션 해야하는 요구사항이 있었다. 마이그레이션 과정에서 지금까지 사용했던 MVC 패턴에 대한 고민이 있었고, 또한 우테코 미션 중 처음으로 웹 UI로 애플리케이션을 구현 하는 미션이다보니 생긴 웹 UI에 대한 궁금증이 있었다.

로또 미션 레포지토리
배포 사이트

MVC 패턴 적용기, 그리고 MVVM 패턴

MVC 패턴

프리코스부터 이번 로또 미션의 step1까지 모든 미션은 콘솔 기반 애플리케이션을 자바스크립트로 구현하는 미션이었다. 지금까지의 미션에서 거의 필수적으로 등장하는 요구 사항이 있었는데,

도메인 로직과 UI 로직을 분리한다.

바로 도메인과 UI를 분리해서 로직을 작성하라는 요구사항이었다. 해당 요구사항을 지키기 위해 MVC 패턴을 도입했다. 도메인 로직을 Model, UI 로직을 View로 역할을 부여하고 두 로직간의 의존성을 아예 분리 시켰다. 여기서 Controller는 두 로직을 서로 연결 시켜주는 역할을 한다.

// View
const InputView = {
  ...
  
  readWinningLottoNumbers() {
    return new Promise((resolve) => {
      readLine.question(MESSAGE.REQUEST_LOTTO_NUMBERS, (userInput) => resolve(userInput));
    });
  },

  readBonusLottoNumber() {
    return new Promise((resolve) => {
      readLine.question(MESSAGE.REQUEST_BONUSE_NUMBER, (userInput) => resolve(userInput));
    });
  },
  
  ...
}

당첨 번호 입력과 보너스 번호 입력에 대한 InputView 로직

// Model
class LottoGame {
  #lottos = [];
  #winningLotto;
  
  ...
  
  generateWinningLotto(lottoNumber, bonusNumber) {
    this.#winningLotto = new WinningLotto(lottoNumber, bonusNumber);
  }

  ...
}

당첨 로또를 생성하는 Model 로직(도메인 로직)

// Controller
class LottoGameConsoleController {
  
  ...
  
  async generateWinningLottoNumbers() {
    try {
      const winningLottoString = await InputView.readWinningLottoNumbers();
      const winningLottoNumber = winningLottoString.split(',').map(Number);
      const bonusNumber = await InputView.readBonusLottoNumber();
      this.#lottoGame.generateWinningLotto(winningLottoNumber, bonusNumber);
    } catch (error) {
      OutputView.printErrorMessage(error);
      return this.generateWinningLottoNumbers();
    }
  }

  ...
}

당첨 번호와 보너스 번호를 입력 받고 당첨 로또를 생성하는 Controller 로직

콘솔 기반 애플리케이션에서 MVC 패턴을 적용하며 MVC 패턴에 대한 장점을 꽤 체감할 수 있었다. 도메인 로직과 뷰 로직을 분리하니 서로의 역할이 분명해졌고, 이에 따라 코드의 유지보수성을 높일 수 있었다. 또한 도메인 로직에 대한 테스트 코드를 훨씬 쉽게 작성할 수 있었다.

하지만 로또 미션 step2에 들어서면서 새로운 요구 사항이 생겼다.

콘솔 기반 애플리케이션을 웹 애플리케이션으로

UI로직을 통째로 바꿔야하는 요구사항이 생겼다. step2에서는 기존 콘솔 기반 로또 애플리케이션을 웹 기반 로또 애플리케이션으로 바꿔야했다. 콘솔 UI를 웹 UI로 변경하는 과정을 거치며 도메인 로직과 UI 로직을 분리하는 방식의 큰 장점을 느낄 수 있었다. 도메인 로직을 하나도 건드리지 않고 UI 로직을 변경할 수 있었다. 또한, 기존에 하던대로 MVC 패턴을 적용하려고 했다.

익숙하지 않은 웹 UI의 구조

하지만 웹 UI와 콘솔 UI에서의 환경은 꽤 많이 달랐다. 콘솔 기반 애플리케이션과 웹 UI 기반 애플리케이션의 작동 방식에는 여러 차이가 있는데, 그 중에서 가장 큰 차이점 중 하나는 동기-비동기의 차이라고 생각한다.

콘솔 기반 로또 게임은 유저의 입력을 받고, 그 다음 입력을 기다리는 과정이 반복되면서 동기적으로 애플리케이션이 실행된다. 이에 반해 웹 UI 기반 로또 게임은 비동기적으로 실행된다. UI 각 요소에 event listener를 적용하고 특정 이벤트가 발생했을 때 해당 기능을 작동하는 방식으로 실행된다.

MVC 패턴이 불편해

이에 따라 기존에 사용하던 MVC 패턴에 크게 두가지 불편함을 느꼈다.

첫 번째로,이벤트 바인딩이나 DOM을 다루는 과정에 대해서 컨트롤러와 뷰에 대한 역할이 애매한 부분이 많았다.

두 번째로, 웹 UI에서는 마우스 클릭, 호버, 터치, 타이핑, 스크롤 등등 수많은 이벤트들이 있는데, 이 이벤트들을 감지하고 모델에 적용하고, 모델이 바뀌면 뷰에 변화를 줘야한다. 뷰와 모델의 변화를 모두 흡수 해야 하다보니 컨트롤러의 크기가 엄청 커지는 문제가 있었다.

MVVM 패턴

MVC 패턴을 대체 할 다른 패턴이 있는지 찾아보던 중, MVVM 패턴이란 것을 알게되었다. MVVM 패턴은 무엇인지, MVC 패턴과는 어떤 차이점이 있는지 학습했다.

MVVM 패턴이란, Model-View-ViewModel의 약자이며, Model과 View는 MVC의 그것과 거의 유사하고 ViewModel이 존재하여 Controller를 대체하는 패턴이며 다음과 같은 특징을 가진다.

  1. 모델의 데이터를 가져와 뷰와 뷰모델에 데이터 바인딩을 한다.
  2. 컨트롤러의 반복적인 기능이 선언 방식으로 바뀐다.

MVC 패턴과 비교하여 MVVM 패턴의 가장 큰 차별점은 데이터 바인딩이라고 생각한다. MVC 패턴의 컨트롤러의 경우 뷰와 모델의 이벤트 감지 및 데이터 변화를 감지하며 뷰와 모델의 중개자 역할을 한다.

반면에 MVVM 패턴의 뷰모델의 경우 모델의 데이터를 뷰에 바인딩 하는 역할을 한다. 데이터 바인딩을 하면 뷰와 뷰모델이 같은 소스의 데이터를 바라볼 수 있다. 이로 인해 모델 혹은 뷰모델의 데이터가 변경될 경우 뷰에도 자동으로 렌더링이 된다. 쉽게말해 데이터가 동기화 된다.

그래서, MVVM 패턴을 미션 구조 설계에 실제로 적용해봤나?

사실 MVVM 패턴을 학습했지만 실제 미션 프로젝트에는 적용해보지 못했다. 기존에 내가 알고있던 수준으로 바닐라 자바스크립트 애플리케이션에 MVVM 패턴을 구현하기에는 너무 어려웠다. MVVM 패턴에 대한 여러 레퍼런스들을 보며 모르는 개념들이 많이 등장했다.

이 개념들을 학습하기에는 아직 남은 미션 요구사항들이 산더미처럼 쌓여있었고, 디자인 패턴 학습에 매몰되어 정작 중요한 미션 요구사항들을 놓치는 것 같은 기분이 들었다. 패턴을 실제로 적용하지 못한 점이 아쉬웠지만 패턴에 대한 학습은 잠시 미뤄두고 요구 사항을 해결해나가며 보다 근본적이고 중요한 부분에 집중하기로 했다.

실제 프로젝트 적용에는 비록 실패했지만 이러한 학습 과정을 거치며 프론트엔드에서 MVC가 더 이상 잘 사용되지 않는 이유를 깨달을 수 있었고, MVC의 단점을 보완하기 위해 나온 패턴들은 어떤 방식으로 문제를 해결했는지 이해할 수 있게 되었다.


웹 UI 다시 익숙해지기

로또 2단계 미션이 우테코에서 처음으로 구현하는 웹 UI 애플리케이션 미션이다. 이 미션을 시작하기만을 기다렸다. 텍스트로만 이루어진 재미없는 콘솔 기반 프로그램을 벗어나 드디어 화면을 개발할 수 있기 때문이다.

하지만 웹 UI를 바로 개발하려고 하니 생각보다 쉽지않았다. 그동안 콘솔 기반 프로그램 개발에 너무 익숙해져 있었다. 게다가 우테코 이전에도 라이브러리를 사용하지 않고 바닐라 자바스크립트로 개발해본지는 정말 오래됐기 때문에 DOM 조작법부터 가물가물했다. 웹 UI에 익숙해질 시간이 필요했다.

웹 UI에 대해 학습하고, 리뷰어에게 피드백 받으면서 몇가지 의문이 생겼었다.

DOM 접근을 왜 최소화 하라는걸까?

바닐라 자바스크립트를 사용하여 웹 앱을 제작할 때 DOM 접근은 거의 필수적이라고 볼 수 있다. 그러나 웹 UI에 대한 피드백 강의 중 DOM 접근을 최소화 하라 라는 피드백이 있었다. 이 피드백을 보고 문득 DOM 접근을 왜 최소화 하라는건지 궁금해졌었다.

가장 큰 이유는 브라우저 렌더링 때문이다. 브라우저가 렌더링 할 때 위 다이어그램의 과정을 거친다. 우리가 만약 DOM에 접근하고 조작할 경우 위 과정을 모두 다시 시작한다. 그렇기 때문에 DOM 조작 작업의 비용은 매우 비싸다고 하는 것이고, 이는 곧 애플리케이션의 성능 저하로 이어지게 된다.

특히 최근 동적인(DOM 조작이 많은) 웹 사이트의 수요가 매우 많아졌고, 이러한 성능 저하를 방지하기 위해 React의 Virtual DOM과 같은 기술들이 탄생하게 되었다.

이벤트 객체는 어떻게 잘 활용 할 수 있을까?

미션을 진행하며 이벤트 리스너 함수를 자주 사용했었다.

element.addEventListener('eventType', callback);

리뷰어분의 피드백에 따르면, 이벤트 리스너 함수의 콜백 함수에서는 이벤트 객체를 인자로 받지만 나의 코드에서는 이벤트 객체를 전혀 활용하지 못한다고 피드백을 해주셨다. 이 피드백을 받고나서 어떻게 해야 이벤트 객체를 잘 활용할 수 있을지에 대해 고민해보고 적용했다.

일단 이벤트 객체를 활용하면 위에서 말한 DOM 접근을 상당히 줄일 수 있었다.

click 혹은 keydown 등의 이벤트에서 event.target을 사용하면 이벤트가 발생한 태그를 쉽게 가져올 수 있는건 기본이고, form 태그의 submit 이벤트에서 event 객체를 활용하면 form 태그 내의 input 값들을 쉽게 가져올 수 있었다.

formElement.addEventListener('submit', (event) => {
	event.target[0].value; // form 태그 첫 번째 input의 value
	event.target.querySelector('.temp') // form 태그에서 temp 클래스를 가진 요소의 value
});

또한 이벤트 위임을 사용하면 더욱 효율적인 코드를 작성할 수 있다. 여기서 이벤트 위임이란, 이벤트 버블링이벤트 캡처링을 활용하여 상위 요소에서 하위 요소의 이벤트들을 제어하는 방식을 의미한다. 이번 미션에서 이벤트 위임을 다음 두가지 경우에서 사용했다.

  1. 클릭한 요소가 원하는 요소가 아닐 경우 무시하기
  2. 여러 개의 자식 요소 이벤트 관리하기
table.addEventListner('click', (event) => {
  const td = event.target.closest('td');

	if (!td) return;
	
	const tdId = td.dataset.tdId;
	handleClicktd(tdId);
})

이번 미션에서는 이벤트 객체를 활용해 DOM 조작 줄이기, 이벤트 위임으로 하위 요소 관리하기 등을 할 수 있었다. 이러한 사용법 외에도 이벤트 객체를 사용하면 훨씬 더 효율적인 웹 개발을 할 수 있을 것이다.

CSS 선택자를 사용할 때 왜 태그 id 보단 class를 선호할까?

이전까지 CSS 선택자를 사용할 때 태그의 id와 class를 섞어서 사용했었다. 사실 id와 class를 쓰는 기준으로는 중복된 요소면 class, 아니면 id를 사용했고 이 외에는 딱히 큰 기준은 없었다. 그런데 다른 사람들의 코드 레퍼런스를 보면 대부분 class를 CSS 선택자로 사용했다. 다른 사람들은 어떠한 기준으로 class를 CSS 선택자로 사용하는지 궁금해졌다. CSS 선택자로 id보다 class를 선호하는 이유는 다음과 같았다.

class 선택자의 재사용성

첫 번째로 class 선택자는 재사용이 가능하기 때문이다. id 선택자는 오직 하나의 요소만 지정이 가능하므로 재사용이 불가능해진다. class 선택자에서는 해당 class의 스타일을 지정하면 두루두루 사용할 수 있고, 재사용이 필요없는 스타일이라도 어느정도의 가능성을 열어두는 것이 좋을 것이다. 또한, 한 요소에서 여러개의 클래스를 가질 수 있기 때문에 여러 클래스의 스타일 요소를 동시에 적용할 수 있다.

선택자 우선 순위

두 번째로 id의 우선순위가 class 보다 높다. id의 우선 순위가 높기 때문에 id의 스타일 값을 class로 덮지 못한다. 이렇게 우선 순위가 다른 선택자를 번갈아가며 사용한다면 우선 순위 전쟁을 벌어야 할 수도 있고, 이는 코드의 규모가 커질수록 심해질 수 있다.

일관성 있는 규칙

마지막으로는 일관성있는 규칙을 가져갈 수 있다. class는 CSS를 위한 것, id는 DOM 조작과 같이 스크립트 작업에 위한 것이라고 인지하며 하나의 코드 컨벤션으로 가져간다면 다른 개발자들이 코드를 이해하기 훨씬 쉬워질 것이다.


참고자료
https://medium.com/technogise/dom-manipulation-in-browser-59b793bee559
https://dev.to/clairecodes/reasons-not-to-use-ids-in-css-4ni4

profile
💻✏️🌱

2개의 댓글

comment-user-thumbnail
2023년 4월 26일

룩소! 글 잘 읽었습니다! 옛 기억이 새록새록 떠오르네요ㅋㅋㅋ 저도 css 선택자에 대한 고민이 많았는데 같은 고민을 하고 계셨군요!

답글 달기
comment-user-thumbnail
2023년 5월 7일

룩소 글 잘 보았습니다~ 저도 css와 id에 대한 기준이 헷갈려서 저만의 기준을 만들었었네여

답글 달기