[우아한테크코스 FE 5기] 레벨1 점심 뭐 먹지 미션 회고

Chex·2023년 3월 13일
0

우아한테크코스

목록 보기
8/19
post-thumbnail

😋 점심 뭐 먹지

📍 학습 목표

  • 어플리케이션을 컴포넌트 단위로 모듈화하여 개발
    • UI를 컴포넌트 단위로 생각하고 개발하는 연습
    • 재사용할 수 있는 컴포넌트를 고민해보기
  • 웹 UI 환경에서의 테스트 기초
    • 컴포넌트 단위 테스트 (1단계)
    • 사용자 관점에서 중요하다고 생각하는 기능을 스스로 정의하고 E2E 테스트로 검증해보기 (2단계)
  • TypeScript의 기본 문법을 익히며 필요성을 경험

🚀 기능 요구 사항

  • step1 - 음식점 목록
캠퍼스 주변의 점심 식사 스팟 목록을 관리하는 앱을 만든다.

- 음식점 목록을 확인할 수 있다.
    - 카테고리별로 필터링해서 확인할 수 있다.
    - 이름순/거리순으로 정렬해서 확인할 수 있다.
- 음식점 목록에 새로운 음식점을 추가할 수 있다.
    - 음식점의 카테고리, 이름, 거리(도보 이동 시간), 설명, 참고 링크를 입력해서 추가할 수 있다.
    - 카테고리, 거리는 셀렉트 박스, 이름/설명/참고 링크는 텍스트 인풋을 사용한다.
    - 카테고리, 이름, 거리는 입력 필수.
        - 카테고리는 "한식", "중식", "일식", "아시안", "양식", "기타" 중 하나를 선택한다.
        - 거리는 캠퍼스로부터 도보로 걸리는 시간(분). 5, 10, 15, 20, 30 중 하나를 선택한다.
    - 설명, 참고 링크는 옵션. 입력하지 않아도 음식점을 추가할 수 있어야 한다.
    - 입력값이 잘못되었을 때 사용자에게 알려주는 방식은 자유롭게 구현한다.
- 새로고침해도 추가한 음식점 정보들이 유지되어야 한다.
  • step2 - 자주 가는 음식점
음식점의 상세 정보를 확인하고, 자주 가는 음식점으로 지정할 수 있는 기능을 추가한다.

- 음식점 상세 정보를 확인할 수 있다.
    - 카테고리, 이름, 거리, 설명, 참고 링크를 확인할 수 있다.
    - 음식점을 삭제할 수 있다.
- 자주 가는 음식점을 추가하고 목록으로 확인할 수 있다.
    - 음식점 목록에서 자주 가는 음식점을 추가할 수 있다.
    - 음식점 상세 정보에서 자주 가는 음식점으로 추가할 수 있다.
    - 자주 가는 음식점 탭에서 추가한 음식점 목록을 확인할 수 있다.
- 새로고침해도 추가한 정보들이 유지되어야 한다.

✅ 프로그래밍 요구사항(이전 미션)

예측 가능하고, 실수를 방지할 수 있는 코드를 작성하기 위해 노력한다.

  • 변수 선언시 const 만 사용한다.
  • 함수(또는 메서드)의 들여쓰기 depth는 1단계까지만 허용한다.
  • 함수의 매개변수는 2개 이하여야 한다.
  • 함수에서 부수 효과를 분리하고, 가능한 순수 함수를 많이 활용한다.

테스트하기 쉬운 코드에 대해 고민하고, 문제를 작은 단위로 쪼개서 접근하는 방식을 연습한다.

  • 모든 기능을 TDD로 구현하는 것을 시도하여, 테스트 할 수 있는 도메인 로직에 대해서는 모두 단위 테스트가 존재해야 한다. (단, UI 로직은 제외)

모듈화에 대해 고민한다.

  • 클래스(또는 객체)를 사용하는 경우, 프로퍼티를 외부에서 직접 꺼내지 않는다. 객체에 메시지를 보내도록 한다.
  • 클래스를 사용하는 경우, 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.

로또미션 1단계

모듈화와 객체 간에 로직을 재사용하는 방법에 대해 고민한다.

  • 로또 번호와 당첨 로또 번호의 유효성 검사시 발생하는 중복 코드를 제거해야 한다.
  • 클래스(또는 객체)를 사용하는 경우, 프로퍼티를 외부에서 직접 꺼내지 않는다. 객체에 메시지를 보내도록 한다.
    • getter를 금지하는 것이 아니라 말 그대로 프로퍼티 자체를 그대로 꺼내서 객체 바깥에서 직접 조작하는 등의 작업을 지양하자는 의미입니다 :) 객체 내부에서 알아서 할 수 있는 일은 객체가 스스로 할 수 있게 맡겨주세요.
  • 클래스를 사용하는 경우, 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.

로또미션 2단계

모듈화에 대해 고민한다. - 도메인과 UI 관심사의 분리

  • 1단계에서 구현한 도메인 로직을 (최대한) 수정하지 않고, UI만 변경한다.
    일관성 있고 의도가 드러나는 마크업을 작성하기 위해 노력한다.
  • 목적에 맞는 HTML 태그를 사용한다.
  • CSS 속성 선언 순서의 일관성을 고려한다.
    CSS 문법 사용에 익숙해진다.
  • CSS 속성은 가능하면 축약형(shorthand)을 사용한다.
  • flexbox를 활용해 레이아웃을 구성한다.

✅ 프로그래밍 요구사항(현재 미션)

컴포넌트 단위로 구현하는 것을 고민하고 적용해본다.

  • 정적으로 렌더링할 영역과 동적으로 렌더링할 영역을 구분해서 고민한다.
  • 재사용할 수 있는 컴포넌트에 대해 고민하고 적용해본다.
  • 컴포넌트 단위 테스트를 적용한다.

도메인 영역을 TypeScript를 사용해 구현한다. (UI 영역은 선택)

  • any를 사용하지 않는다.
  • interface 또는 type alias 를 이용하여, 주요 도메인 객체의 타입을 정의하고 설계한다.

데이터 유지를 위해 localStorage를 활용한다.

개발한 앱에서 핵심이 되는 기능이라고 생각하는 기능 플로우를 선정하고 그에 대한 E2E 테스트를 추가한다.


🔗 관련 링크


📖 배운점

1. 이벤트 위임과 closest에 관하여

// 리팩터링 전 코드
  renderRestaurantList(restaurants: Restaurant[]) {
    const restaurantListContainer = $<HTMLUListElement>('.restaurant-list');
    const restaurantItems = RestaurantList(restaurants);

    restaurantListContainer.innerHTML = '';
    restaurantListContainer.insertAdjacentHTML('beforeend', restaurantItems);

    this.addEventHandlersAfterRenderRestaurant();
  }

  addEventHandlersAfterRenderRestaurant() {
    this.addFavoriteButtonClickEventHandler();
    this.addRestaurantListClickEventHandler();
  }

  addRestaurantListClickEventHandler() {
    const restaurantList = $$('.restaurant');

    restaurantList.forEach((restaurantItem) => {
      restaurantItem.addEventListener('click', (event) => {
        if (!(event.currentTarget instanceof HTMLLIElement)) return false;
        if (event.target instanceof HTMLButtonElement) return false;
        if (this.restaurantDetailModal.open) return false;

        const name = event.currentTarget.querySelector('.restaurant__name')?.textContent;
        const restaurant = JSON.parse(localStorage.getItem(name ?? '') ?? '{}');

        renderRestaurantDetailModal(restaurant);
        this.restaurantDetailModal.showModal();
      });
    });
  }

문제점1. 새로 추가된 음식점의 경우 이벤트 동작X

  • 음식점<li>요소를 클릭했을 때 상세정보를 보여주는 기능을 구현하던 중

  • 이벤트 리스너를 <li> 요소에 추가했기 때문에 새로운 음식점을 추가하면 새로 추가된 음식점 요소에서는 클릭 이벤트가 제대로 동작하지 않았다.

  • 그래서 새로운 음식점 <li>가 추가될 때마다 addRestaurantListClickEventHandler()를 불러와 <li>요소에 이벤트리스너를 추가해줘야하는 문제가 있었다.

해결방안1. 이벤트 위임

이벤트 위임의 장점?

  • 새로운 요소를 추가하더라도 이벤트에 대한 처리는 부모 요소에 위임했기 때문에 새로운 요소에 이벤트 핸들러를 다시 지정할 필요가 없다.

미션에 적용한 부분?

  • <li>의 부모요소인 <ul>에 이벤트리스너를 추가해주면 새로운 음식점 <li>이 추가되어도 이벤트리스너를 또 추가해주지 않아도 될 것이라고 생각하여 부모 요소인 <ul>로 이벤트를 위임하고자 했다.

문제점2. 클릭한 요소에서 음식점 이름 찾기

// RestaurantItem.ts
export const RestaurantItem = (restaurant: Restaurant, categoryImageUrl: string) => {
  const { category, name, distance, description, favoriteImageUrl } = restaurant;

  return `<li class="restaurant">
  
  <div class="restaurant__category">
    <img src="${categoryImageUrl}" 
    alt="${category}" class="category-icon"/>
  </div>

  <div class="restaurant__info">
    <h3 class="restaurant__name text-subtitle">${name}</h3>
    <span class="restaurant__distance text-body">캠퍼스부터 ${distance}분 내</span>
    <p class="restaurant__description text-body">${description ?? ''}</p>

    <button type="button" class="favorite-button" style="background-image: url('${favoriteImageUrl}')"></button>
  </div>

  </li>`;
};
  • 음식점 <li>요소를 클릭하면, 로컬스토리지의 키 값인 음식점 이름으로 사용자가 클릭한 음식점 정보를 renderRestaurantDetailModal에 넘겨준 후 상세정보 모달창을 띄워주어야 했다. querySelector('.restaurant__name')?.textContent;
  • 그런데 <ul>에 이벤트리스너를 달면 event.currentTarget.querySelector(‘.restaurant__name’)의 결과가 음식점 목록에 있는 여러 개의 <li>중 제일 첫번째 요소의 음식점 이름만 나오는 문제가 있었다.
  • currentTarget 대신 target을 쓴다고 해도 event.target.querySelector(‘.restaurant__name’)을 한다면,
  • 만약 사용자가 카테고리 이미지를 클릭했을 때, html 코드 구조 상 event.target 내부에는 .restaurant__name클래스 속성을 가진 요소가 없어 null이 나오기 때문에 이것도 해결 방법이 될 수는 없었다.

문제해결2-1. 노가다

export const getRestaurantNameFromEventTarget = (event: Event) => {
  // 음식점 li요소 클릭
  if (event.target instanceof HTMLLIElement) {
    return event.target.querySelector('.restaurant__name')?.textContent;
  }

  // 음식점 카테고리 div요소 클릭
  if (event.target instanceof HTMLDivElement) {
    return event.target.nextElementSibling?.querySelector('.restaurant__name')?.textContent;
  }

  // 음식점 카테고리 이미지 클릭
  if (event.target instanceof HTMLImageElement) {
    const name =
      event.target.parentElement?.nextElementSibling?.querySelector(
        '.restaurant__name',
      )?.textContent;
    return name;
  }

  // 음식점 이름 클릭
  if (event.target instanceof HTMLHeadingElement) {
    return event.target.textContent;
  }

  // 음식점까지의 거리 클릭
  if (event.target instanceof HTMLSpanElement) {
    return event.target.previousElementSibling?.textContent;
  }

  // 음식점 설명 클릭
  if (event.target instanceof HTMLParagraphElement) {
    return event.target.previousElementSibling?.previousElementSibling?.textContent;
  }
};
  • <ul>태그에 이벤트리스너를 달아준 후, 모든 경우를 다 나눠서(<li>요소를 클릭했을 때, 카테고리 <div>요소를 클릭했을 때, 카테고리 이미지를 클릭했을 때, 이름을 클릭했을 때, 거리를 클릭했을 때, 설명을 클릭했을 때) 음식점 이름을 탐색해주었다.

문제해결2-2. closest 사용

export const getRestaurantNameFromEventTarget = (event: Event) => {
  if (event.target instanceof HTMLElement) {
    return event.target.closest('.restaurant')?.querySelector('.restaurant__name')?.textContent;
  }
};
  • closest자신을 포함한 상위 요소를 탐색한다는 점을 이용하여, event.target의 상위 요소인 .restaurant를 찾고 그 하위에 있는 .restaurant__name을 찾아오는 방법으로 해결했다.

깨달은 점?

  • 이벤트 전파 중 이벤트 캡처링 단계에서는 이벤트가 document root부터 target element까지 내려온다.
  • 그러면 이벤트는 target element의 모든 부모 요소를 지나가게 되니까 이벤트 위임이 가능한 거구나!

2. 컴포넌트에 관하여

컴포넌트를 어떻게 정의했는가?

1단계

리뷰어 서니가 1단계 코드리뷰에서 내가 생각하는 컴포넌트가 무엇인지를 물었다.

컴포넌트란

나는 컴포넌트란 사용자에게 보여지는 화면을 각자의 기준에 따라 독립적인 조각으로 나눈 개발단위라고 했다.

애슐리와 나는 1단계에선 RestaurantItemRestaurantList 템플릿을 컴포넌트로 정의했다. 그리고 1단계 테스트를 진행하던 중 ‘템플릿만을 컴포넌트로 정의하는 것은 컴포넌트의 범위를 너무 좁게 생각했던 것이었나?’ 하고 깨닫게 되었다.

1단계 테스트에서는 Cypress 같은 도구 없이 컴포넌트에 대한 테스트를 진행해야 했다. 그런데 우리가 구현한 코드에서는 이벤트를 다루는 함수가 컴포넌트 내부에 있는 것이 아니라 외부에 있었기 때문에 이벤트를 발생시키기가 어려워 UI테스트를 작성하지 못했다. 그 부분에 대해서도 서니에게 질문을 했었는데 아래와 같은 답변을 받았다.

컴포넌트 테스트

‘아 내가 작성한 컴포넌트가 UI 컴포넌트였구나!’ ㅎ ㅏ ㅎ ㅏ ㅎ ㅏ
이제야 컴포넌트에 대해 감이 잡혔다.

2단계

점심 뭐 먹지 2단계 미션에서는 컴포넌트를 구성하는 방식을 구조+스타일+기능을 묶은 단위로 넓게 잡아보았다.

📦components
 ┣ 📜RestaurantDetailModal.css
 ┣ 📜RestaurantDetailModal.ts
 ┣ 📜RestaurantItem.ts
 ┣ 📜RestaurantList.css
 ┣ 📜RestaurantList.ts
 ┣ 📜TabItem.css
 ┗ 📜TabItem.ts

잘 모듈화된 컴포넌트의 특징?

  • 하나의 명확한 역할을 하며
  • 재사용하기 쉽고
  • 조립해서 필요한 UI를 구성하기 쉽게 만들어 줄 수 있다.

잘 모듈화된 컴포넌트는 위 세가지 특징을 가지고 있다고 하는데, 2단계에서 만든 컴포넌트들이 저 기준을 완전히 충족하는지는 모르겠다. 하지만 탭 메뉴에 들어가는 TabItem 컴포넌트는 조금 만족스럽다.

export const TabItem = (tabInfo: { tabId: string; tabTitle: string; isChecked: boolean }) => {
  const { tabId, tabTitle, isChecked } = tabInfo;
  const checked = isChecked ? 'checked="checked"' : '';

  return `
    <input type="radio" name="tab" class="tab-link" id="${tabId}" ${checked} />
    <label for="${tabId}" class="text-subtitle">${tabTitle}</label>`;
};

탭에 관한 정보들을 인자로 받아 재사용할 수 있게 만들었기 때문이다. 우하하.

✏️ 느낀점

1. 웹 컴포넌트라는 기술부채

컴포넌트란 무엇인가에 대해 정의부터 고민했던 미션인 만큼 다른 크루들이 사용했던 웹 컴포넌트 기술을 사용해보진 못했다. 그래서 이렇게 또 기술부채가 쌓였다. 빚 상환은 언제할까? ㅎㅎ..

cf) 웹 컴포넌트

  • Custom Elements, Shadow DOM, HTML Templates, ES6 Modules 와 같은 표준 스펙을 활용해 컴포넌트를 구성할 수 있게 해주는 웹 기술

2. 생각 생각 생각! 나만의 이유 만들기

매번 미션을 진행하면서 느끼는 것인데 ‘이렇게 하는 게 맞나?’, ‘다른 사람들은 어떻게 하지?’ 하고 고민하며 보내는 시간이 많은 것 같다. 이제 고민만 하지 말고 나만의 이유를 만들어 봐야겠다. 누군가 그 이유를 물었을 때, 나는 왜 그렇게 했는지 명확하게 말할 수 있도록.

가끔 숲이 아닌 나무만 보는 경향이 있는데, 가끔 위에서 내려다 보며 ‘나는 지금 어디에 있는지, 뭐 하고 있는지’ 생각해보는 시간을 갖도록 하자.

3. 회고록 형식 고민

코드리뷰를 받은 내용들을 다시 정리해보면 좋을 것 같아서 다음부터는 그 내용들을 담아보면 어떨까 하고 고민 중이다.

4. 건강관리 잘해

건강관리 잘해

이번 미션을 진행하면서 몸 컨디션이 별로 좋지 않아서 골골댔는데 이렇게 몸이 아파오기 시작하는 걸 보니, 나 자신.. 이곳에 열심히 적응 중인가보다. 건강 관리를 위해 종합비타민을 먹어야겠다고 다짐. 피로회복에는 비타민B! 그중에서도 비타민B 함량이 높은 비맥스메타를 추천 받아서 퇴근 길에 구입했다. 만성피로야 물럿거라.

Be the best version of you!

profile
Fake It till you make It!

0개의 댓글

관련 채용 정보