[우테코 6기 FE] 로또 게임 회고

바다·2024년 3월 3일
1

💫우테코_6기

목록 보기
2/6
post-thumbnail

안녕하세요. 우테코 6기 FE 크루 바다 입니다. 😁🐳

우테코 프론트엔드 레벨1의 두 번째 미션인 "로또 게임"을 완료했습니다. 🎉🥳🎉

"로또 게임"미션을 구현하면서 발생한 이슈와 해결방법들, 알게 된 점과 느낀 점에 대해 이야기 해보려합니다.

우테코 레벨1의 미션은 페어와 함께 하는 1단계와 1단계의 도메인 로직을 최대한 유지하면서 혼자 구현하는 2단계로 나누어져 있습니다.

✋ 잠깐!

밑에서 예시를 든 코드들은 실제 미션 구현 시 사용된 것 이 아닌 설명을 위해 간략하게 적은 코드들 입니다.

🎮1단계 - 콘솔 기반 로또 게임

우테코 학습 목표

  • UI와 도메인 영역을 분리한다.
  • TDD를 적용해 단위 테스트 기반으로 점진적인 리팩토링을 시행한다.

개인적인 학습 목표

  • TDD를 적용하여 페어와 기능을 구현한다.

구현

test.each에서 테이블의 요소들을 화면에 찍는 법

jest는 테스트 결과나 로그를 시작적으로 쉽게 이해할 수 있게 프린트 포맷팅 기술을 지원하고 있습니다.

Generate unique test titles by positionally injecting parameters with printt formatting

test.each(table)(name, fn, timeout)

table의 요소의 타입에 따라서 %s(String), %d(Number), %i(Integer)등의 문자를 활용하면 test.each의 table의 어떤 요소에 대한 테스트인지를 확인할 수 있습니다.

  • test.js
test.each(['1/2/3/4/5/6', '1 2 3 4 5 6'])(
      '당첨 로또 번호는 쉼표(,)로 구분되어 있지 않으면 오류가 발생한다.\n [Test Case] : %s \n',
      (input) => {
        expect(() => new WinningLotto(input)).toThrow(
          ERROR_MESSAGES.inValidWInningNumbersForm,
        );
      },
    );
  • 테스트 로그

test.each에 대한 jest 문서 보러가기

느낀 점 : TDD는 길잡이

코드를 구현 하기 전에 모듈 구조,프로세스에 대해 계획을 하는 편이라 TDD로 작은 단위부터 작성하면 과연 전체적인 구조가 잡힐까?라는 의구심이 있었습니다.

하지만 미션을 진행할 수록 TDD가 "길잡이"역할을 한다는 것을 느꼈습니다. 혼자 기능 구현을 하다보면 구현 중 오류를 해결하거나 한 번 코드를 짤때 깔끔하게 짜고자 하는 욕심에서 "어? 내가 뭐하려고 했지?","아~ 이거 하던 중이 였는데" 혹은 "이 기능이 들어있어야 하는데 빠졌네"라며 길을 잃는 경우가 생깁니다. 하물며 노를 젓는 개발자가 2명인 페어와의 미션에는 길을 잃는 경우가 훨씬 많습니다. 이럴 때 TDD는 지금 당장 구현해야하는 것에 대해 테스트 케이스를 만들고 해당 테스트를 통과하는데 집중하게 해서 길을 잃는 경우가 현저히 적었고 TDD에 익숙해질 수록 구현 속도가 빨라졌습니다.


🎮 2단계 - 웹 기반 로또 게임

우테코 학습 목표

  • UI와 도메인 영역을 분리할 수 있는 설계를 고민해보고, 목적에 맞게 객체와 함수를 활용
  • 단위 테스트 기반으로 점진적인 리팩터링한다.

개인적인 학습 목표

  • HTML 시맨틱 태그를 사용한다.
  • 스크린 리더기를 생각해 input의 label을 넣는다.
  • 많이 사용하는 css 속성들은 변수로 만들어서 재사용한다.
  • 반응형 웹을 만든다.
  • 유저의 편의성(UX)을 생각한다.
    • 피그마에 없지만 사용자가 입력값에 대한 요구사항과 오류를 알 수 있도록 토글 메세지와 오류 메세지를 화면에 띄운다.
    • 입력값에 맞는 형식을 작성할 수 있도록 input의 type과 min,max 속성을 활용한다.
    • 게임 결과에 대한 팝업창을 열면 스크롤을 움직여서 팝업창의 시작점에서 팝업창을 볼 수 있도록 한다.
  • 1단계에서 만든 상수 객체를 활용해 HTML의 내용을 만든다.(EX:구매 금액의 요구 사항등)

구현

오류과 해결 방법

webpack server 실행 시 dist 폴더내의 파일들이 삭제되는 오류

오류 원인

웹팩 서버는 실행 시 메모리 상에 파일을 보관하고, 실제 물리적인 dist 폴더에는 파일을 남기지 않습니다.그 결과 dist 폴더에 파일들이 빌드 되어도 웹팩 서버가 열리면 삭제됩니다.

해결 방법

webpack.config.js에서 웹 팩 서버를 열 때도 dist 폴더에 파일을 저장하도록 설정하도록 옵션을 수정합니다.

module.exports = {
  // ... 생략 ...
  devServer: {
    devMiddleware: {
      writeToDisk: true,
    },
  },
  // ... 생략 ...
};

빌드 시, index.html에서 css,js 파일 경로 오류

오류 원인: 빌드 시, 열리는 index.html은 dist/index.html이다.

//webpack.config.js
//...
 entry: './src/step2-index.js',
//....
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      template: './index.html',
      inject: 'body',
      favicon: './favicon.ico',
    }),
  ],
//...

root/index.html에서 css,js를 불러오면 해당 파일들의 경로를 찾아올 수 없다는 오류가 뜨는데 이유는 개발 시 사용하는 index.html은 root 또는 src 내의 html이지만 웹 팩 서버나 깃허브 페이지에서 열리는 index.html은 dist 폴더 내의 html이기 때문에 개발 시 사용된 index.html에서 상대경로는 dist/index.html의 상대경로와는 다르기 때문에 오류가 납니다.

해결 방법

해결 방법은 절대 경로로 파일들을 설정하거나, 빌드의 진입점인 step2.index.js에서 css,js파일들을 여는 것 입니다.

Uncaught TypeError: attempted to get private field on non-instance

오류 설명

TypeError: attempted to get private field on non-instance 오류는 클래스의 프라이빗 필드에 대한 접근이 해당 클래스의 인스턴스가 아닌 다른 곳에서 이루어졌음을 의미합니다.

오류 원인

클래스내의 private 메서드가 이벤트 리스너에서 콜백 함수로 사용되면 이때 this의 컨텍스트가 해당 클래스의 인스턴스를 잃어버립니다.

오류 해결

이벤트 리스너의 callback 함수(이하 콜백함수)를 bind(this)로 묶으면 됩니다.

  #addEvent() {
    this.$btnPayLottoEl.addEventListener(
      'click',
      this.#handleClickBtn.bind(this),
    );
  }

  #handleClickBtn() {
    // 버튼 클릭 시 일어나는 기능들....
  }

이벤트 리스너의 콜백함수와 arrow funtion, bind(this)

문제: addEventListener의 콜백 함수의 경우 언제는 bind로 묶고 언제는 묶지 않아야 하는가?🤔

답: 콜백 함수가 arrow function 이라면 bind로 묶지 않아도 됩니다.

  • 콜백함수?
    함수를 다른 함수의 인자로 전달하고, 그 함수 내에서 해당 인자로 받은 함수를 실행하는 것을 의미합니다.

js에서 함수를 다른 함수의 인자로 전달할 때 전달된 함수는 실행될 때의 컨텍스트(=함수가 참조하는 this의 값)은 특별하게 설정하지 않는 이상 기본적으로 글로벌 컨텍스트(브라우저에서는 window)가 됩니다.

addEventListenr의 콜백 함수의 this도 글로벌 컨텍스트일까요? 아닙니다. addEventListenr의 콜백 함수의 this는 해당 이벤트가 적용되는 element를 가리킵니다.

그렇다면 arrow function의 this는 무엇일까요? arraow function은 자신만의 this를 가지지 않고 상위 스코프의 this를 사용합니다.

class LottoMachineCotroller(){
  //...
 #addEvent() {
     this.$btnPayLottoEl.addEventListener(
       'click',
       //arrow function :(event)=>this.#handleClickBtn(event),
       this.#handleClickBtn.bind(this),
     );
   }
#handleClickBtn() {
    // 버튼 클릭 시 일어나는 기능들....
  }
 //...
}

this.$btnPayLottoEl.addEventListener(event, callBack)에서 콜백함수의 this는 이벤트 리스너의 대상인 $btnPayLottoEl를 가리켜서 콜백함수가 this.#handleClickBtn로 클래스의 메서드를 그대로 가져와 실행하는 경우라면 bind(this)로 handleClickBtn가 있는 LottoMachineController를 묶어주어야합니다. 반면에 콜백 함수에 클래스의 메서드를 이용한 arrow function을 사용한다면 arrow function의 this는 상위 스코프인 LottoMachineController가 되어서 bind 할 필요가 없습니다.

💡 정리

  • 콜백 함수에서 클래스/객체의 메서드를 그대로 가져와 사용한다면 bind(this)로 클래스/객체를 this로 설정해주어야 한다.
  • 콜백 함수에서 arrow function을 사용한자면 bind(this)를 할 필요 없다.

html에 prettier 적용하기

이미 prettierrc에서 prettier를 적용한고 있다는 것을 전제로 아래의 단계를 진행합니다.

1. .vscode/setting.json에서 html의 포맷터를 vsCode로 지정한다.

"[html]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  //html 저장 시 자동으로 prettier 사용해서 포맷을 적용함
  }

2. vscode의 html 기본 포맷터를 prettier-Code formatter로 지정한다.

  1. F1키를 눌러서 Format Document width (문서 서식 프로그램)을 연다.
  2. Configure Default Formattter(기본 포맷터 구성)으로 들어간다.
  3. Prettier - Code formatter를 설정한다.

form 의 유효성 검사

form 의 유효성 검사를 통과하지 못하면 submit 기능이 중단됩니다.

HTML5에서 form은 required 속성이 있는 form 하위 요소에 작성된 입력값에 대한 유효성 검사를 수행하고, 유효하지 않을 경우 submit 이벤트가 발생하더라고 폼 전송이 중단되어 사용자에게 입력값 오류를 보여줍니다.

novalidate :form의 유효성 검사 기능 끄기

form의 입력값에 대한 유효성 검사를 진행하는 모듈이 따로 있고 유효성 검사의 결과에 따라 진행해야하는 로직들을 다르게 설정하도록 구현했습니다. (통과-> 게임의 다음 단계 진행, 통과x-> 하단에 오류 메세지 출력)
따라서 form 자체의 유효성 검사를 비활성화하기 위해서 form에 novalidate속성을 추가했습니다

<form id="myForm" novalidate>
  	<input type="number" min="1" max="45" maxLength="2" required />
  	<input type="submit" value="제출">
</form>

서버에 데이터를 보내는 것도 아니고,form의 유효성 검사를 진행하지도 않는데 form 태그를 사용해야할까?

form은 서버에 사용자의 입력값을 보내고 form 자체에서 유효성 검사를 진행하는 유효한 기능을 가지고 있습니다. 이 뿐만 아니라 아래와 같은 웹 접근성 측면에서의 이점들도 가지고 있습니다.

  1. 스크린 리더기 사용자들에게 폼의 존재를 보다 뚜렷하게 전달한다.
  2. form 태그를 사용하면 키보드로 form 내의 요소들을 탐색하는 것이 용이해 키보드나 보조 기술을 사용하는 사람들에게 필수적인 요소 입니다.

formEl.reset()과 event.preventDefault()

formEl.reset()?

form 태그에는 form의 하위 요소들(input등)의 값을 초기화하는 reset 기능이 있습니다.

  • html
<form id="myForm">
  	<!-- 폼 입력 요소들 -->
  	<input type="reset" value="초기화">
</form>
  • js
document.getElementById('myForm').reset();

event.preventDefault는 reset을 통한 초기화를 막는다.

#addEvent(){
 $formElement.addEventListener('reset',(event)=>this.#handleReset(event))
}
//....

#handleReset(event){
	event.preventDefault();
// reset 시 실행할 로직들.... 
}

event.preventDefault()는 js에서 이벤트가 기본적으로 가진 기본 동작을 방지하기 위해 사용되는 메소드로 브라우저의 특정 요소에 설정된 기본 동작이 실행되는 것을 막고 개발자가 정의한 동작을 실행할 때 사용됩니다.

reset이벤트 시 실행되는 콜백 함수에 event.preventDefault가 있다면 reset의 기본 기능인 초기화를 막아서 원했던 초기화 기능이 실행되지 않습니다.

DOM에 대한 접근을 최소화하자!

  • 구현 시 DOM에 접근해서 요소를 생성,수정,삭제해야 하는 상황
    • 1단계에서 게임 룰에 대해 정의한 상수를 활용해서 input의 숫자에 대한 속성들 설정(min,max,maxLength등)
    • 1단계에서 게임 룰에 대해 정의한 상수를 활용해서 게임 룰에 대한 안내 문구(토글)생성
    • 게임 단계 별 화면에 표시되어야하는 값들(발행된 로또 값,당첨 번호와 보너스 번호 입력폼, 로또 게임 통계)

문제: 반복문을 사용해 DOM에 대해 접근하는 횟수가 많다.

// 배열의 요소들입니다.
const items = ['첫 번째 항목', '두 번째 항목', '세 번째 항목'];

// 부모 요소를 찾습니다.
const parent = document.getElementById('parentElement');


// 배열을 순회하며 div 요소를 만듭니다.
items.forEach(item => {
  const div = document.createElement('div');
  div.textContent = item;
  parent.appendChild(div);
});

해결 방법1 : DocumentFragment와 insertAdjacentHTML

  • DocumentFragment?
    DocumentFragment는 일반적인 DOM 트리의 일부가 아니라 요소를 추가해도 페이지에는 아무 변화도 생기지 않으며 이를 이용하면 여러 요소들을 DOM에 추가하기 전에 임시적으로 보관하고 한 번에 DOM에 추가할 수 있습니다. 많은 수의 요소를 추가해야 할 때 성능상의 이점이 있습니다.
const items = ['첫 번째 항목', '두 번째 항목', '세 번째 항목'];
const parent = document.getElementById('parentElement');

// DocumentFragment 인스턴스를 생성
const fragment = document.createDocumentFragment();

// 배열을 순회하며 div 요소
items.forEach(item => {
  const div = document.createElement('div');
  div.textContent = item;
  fragment.appendChild(div);
});

// 모든 div 요소들을 한 번에 부모 요소에 추가
parent.appendChild(fragment);
  • insertAdjacentHTML?
    insertAdjacentHTML는 HTML요소에 특정한 위치에 HTML 또는 XML을 삽입할 때 사용하는 메서드 입니다.

  • DocumentFragment와 insertAdjacentHTML 사용하기
    컴포넌트를 만들어서 DOM에 넣어주고 싶다면 DocumentFragmentinsertAdjacentHTML를 같이 활용하면 좋습니다.

  • 예시:
const HtmlInjector= {
  //....
  private_getHtml() {
    
    return `
		<div>
			 html 에 넣을 요소들.....
		</div>
    `;
  },

 private_injectHtml() {
    const parentElement = document.querySelector(
      '.parent',
    );

    const html = this.private_getHtml();
	//parentElemnt의 마지막 요소로 삽입
    parentElement.insertAdjacentHTML('beforeend', html);
  },
};
}

해결 방법2 : 추상화를 사용한 setAttribute와 DocumentFragment

특정 element에 대해 여러 속성들을 넣어주기 위해서 속성들을 추상화한 후 setAttribute와 DocumentFragment를 사용했습니다.

const HtmlInjector={
//...
 
 private_setAttributes(){
   const parentElement = document.querySelector(
     '.parent',
   );
   const fragment = document.createDocumentFragment();
   const div= document.createElement('div');
   // 속성들을 하나의 객체로 추상화
 	const attributes ={
     id:'아이디',
     className:'클래스',
     ......
   };
     
   // 요소에 속성 넣기
	Object.entries(attributes).forEach(([key,value])=>{
     	div.setAttributes(key,value);
   })
 
 }
}

CSS 변수를 사용하자

폰트,색상 등 스타일 설정 시 자주 사용되는 값들이 있다면 CSS의 변수기능을 사용하면 매우 편리합니다.

:root{
 /* --설정할_스타일의_key:value */
 --lotto-primary-color: #4e5ba6;
}

css 변수를 활용해 색상의 투명도를 조절하고 싶다면?

**rgba(var(변수키), 투명도)**를 이용하면 됩니다. 그러나 여기서 주의할 점은 색상을 나타내는 css의 변수값은 rgb여야 한다는 것입니다. #으로 시작하는 헥사(Hex) 색상값은 rbga에 사용될 수 없습니다.


마무리

이번 미션은 1단계의 모듈들을 최대한 변경하지 않고 웹 기반으로 바꾸는 (html에 js를 엮는 부분)이 가장 고비였던 것 같아요. 왜 많은 서비스들이 바닐라 js가 아니라 js라이브러리/프레임 워크를 사용하는 지 알 것 같더라구요. 가스레인지/인턱션/에어프라이어로 조리하다가 성냥으로 아궁이 불 떼서 요리하는 느낌이 였어요.🥲

늘 그렇지만 이번에도 저의 부족했던 부분을 알게 되고 메꾸는 시간들을 보냈습니다.
오류를 만나고 왜 나지?라며 해결해 갔던 시간들은 힘들었지만 목표했던 시간보다 빨리 구현했고 무엇보다 리뷰어에게 UX에 대한 칭찬을 많이 받아서 기분이 좋습니다.

셀프 칭찬 시간 😊👏

그럼. 다음에도 찾아뵐게요. 🖐️))

profile
🐣프론트 개발 공부 중 (우테코 6기)

2개의 댓글

comment-user-thumbnail
2024년 3월 15일

아궁이에 불 떼서 요리하는 느낌 인상적인 표현이었습니다 ㅋㅋ

1개의 답글