[우아한테크코스 FE 5기] 레벨1 영화 리뷰 미션 회고

Chex·2023년 4월 10일
0

우아한테크코스

목록 보기
9/19
post-thumbnail

회고 양식을 바꿔보려고 한다. 이전 미션 회고에서는 학습 목표, 기능요구사항 등 세부적인 내용까지 다 적었었는데 나중에 회고록을 다시 볼 때 이 부분은 전혀 안보게 된다는 것을 느끼고 회고 내용에서 빼기로 했다. 회고의 주된 내용은 미션을 진행하면서 겪었던 문제 상황, 해결 과정(삽질 과정..), 코드리뷰를 통해 배운점 등을 다뤄보기로 했다.

영화 리뷰 미션은 우아한테크코스 레벨1 기간동안 가장 성장에 대한 고민을 많이 했던 미션이었다. 그 중에서 가장 큰 부분을 차지했던 것은 프로그램 설계에 관한 것이었다. 특히 프로그램의 구조.. 레벨1 미션들을 구현하면서 MVC패턴을 사용하지 않기 위해 Controller라는 용어를 쓰진 않았지만 결국 App이 컨트롤러처럼 쓰이고 있다는 것을 깨닫고 좌절(?)했다. 좌절이라기 보단 내 설계능력의 한계에 답답함(+분함)을 많이 느꼈던 것 같다. 모든 미션의 프로그램 구조가 비슷했기 때문에 스스로가 성장했다고 느껴지지 않았기 때문이다. 그러던 중 리뷰어에게 조언을 받아 문제의 원인을 찾아보는 것부터 시작하게 됐다.

(1단계 코드리뷰 코멘트 中)
App이 액션에 따른 비동기 통신 역할을 수행하게 되니 모든 컴포넌트가 App을 아주 강력하게 의존하는 상황이에요. 이 상태로 확장되다가는 렌더링 뿐만 아니라 상호 참조의 위험도 있답니다.
코멘트에 남긴 부분을 확인해주시고, 우선 첵스가 할 수 있는 부분부터 조금씩 쪼개보면 다음 방향성을 물색할 수 있을 거에요. 아래 부분을 고민해보면 좋겠습니다.

  • 최상위 계층에서 어떤 역할만 담당하게 하는 것이 좋을지
  • 계층 구조에서 서로 간의 통신을 무엇으로 해소할지

🎬 모든 컴포넌트와 의존관계인 App을 어떻게 가볍게 만들 수 있을까?

기존 구조도

https://user-images.githubusercontent.com/24777828/227872761-78d01ecb-7b96-409c-b3cd-a4db860e61f0.png

문제점?

App이 모든 컴포넌트와 의존관계를 이루고 있다.

App이 무거워진 이유는 무엇일까?

https://user-images.githubusercontent.com/24777828/227873669-1946f47e-4e73-4989-a208-f7e8e7f79fff.png

컴포넌트들 간의 의존관계를 줄이기 위해 컴포넌트 간의 소통이 대부분 App을 통해서 이루어졌기 때문이다.

  • Header에서 submit이벤트가 발생하면 App의 onSubmitSearchForm이 실행되고 MovieList를 업데이트한다.
  • MovieListContainer에서 click이벤트가 발생하면 App의 onClickLoadMoreButton이 실행되고 MovieList를 업데이트한다.

그렇다면 무엇을 해야할까?

App이 이벤트 핸들링하는 구조를 개선하기
  • 이벤트 수신과 발신만을 담당하는 EventBroker를 만들고 CutromEvent를 이용하기
  • 만들어준 CustomEvent 종류: updateMovieListEvent, appendMovieListEvent, clickMovieEvent
fetch해온 결과를 받고 화면에 렌더링하는 일은 App이 아닌 해당하는 컴포넌트에게 맡기기

https://user-images.githubusercontent.com/24777828/227872774-9bda3079-41cd-41e8-b83c-6053017d7ff1.png

MovieFetcher와의 소통은 MovieListcontainer가, MovieDetailFetcher와의 소통은 MovieInfoHub를 거쳐 MovieDetailModal이 하도록 수정하여 전체적인 구조를 변경할 수 있었다.

🎬 비동기 통신 관련 에러 핸들링에 관하여

1단계에서는 fetch해 온 후 여러 경우에 따라 반환하는 결과가 다 달라 받는 쪽에서도 로직이 복잡해지는 문제가 있었다. 이때의 코드는 내가 봐도 이해가 잘 안될 정도다. 그래서 2단계에서는 fetch 해온 결과에 대해 여러 정보를 담아서 반환하되 일관된 결과를 반환하도록 수정했다.

// MovieFetcher.ts
async parse(response: Response): Promise<ResponseParsedData> {
    const {
      status_code,
      errors = ['Not Valid Error'],
      total_pages = 0,
      results = [],
    }: APIMovieResponseData = await response.json();

    if (results) {
      return {
        statusCode: STATUS_CODE.SUCCESS,
        statusMessage: API_STATUS[STATUS_CODE.SUCCESS][1],
        totalPages: total_pages,
        rawMovieList: results,
      };
    }

    if (!status_code) return { statusCode: undefined, statusMessage: errors[0] };

    return { statusCode: status_code, statusMessage: API_STATUS[status_code][1] };
  }

2단계에서 영화 별 상세정보를 fetch 해오는 것이 추가되어 MovieFetcher를 재사용할 수 있도록 추상화를 하고 싶었지만 시간이 부족해 시도해보지는 못했다. 대신 영화 상세정보의 경우 fetch한 결과를 컴포넌트에서 바로 받아 렌더링 하지 않고 MovieInfoHub 라는 도메인 객체를 하나 만들어 전달하도록 해보았다.

하지만.. 추후 API가 달라져도 유지보수 하기 쉽도록, 중간에 도메인 객체를 만든 것인데 지금 다시 생각해보니 내가 만든 MovieInfoHub 자체는 별로 하는 일이 없고 Fetcher들이 API에서 정보도 받아오고 도메인에 해당하는 값들을 가공하는 역할까지 여러 가지 일을 하고 있다..

Fetcher는 API에서 정보를 받아오는 역할만 하고 받아온 데이터들을 가공하는 일(API 키 값 수정 후 정제된 데이터로 만드는 일 등)은 MovieInfoHub가 했다면 API가 달라져도 도메인에 해당하는 값을 관리하는 부분을 담당하는 MovieInfoHub만 수정하는 방식으로 대응이 가능할 것 같다.

🎬 localStorage에 저장된 사용자 별점을 컴포넌트 스스로 가져와야할까?

// MovieDetailModal.ts

private getStarCount(movieId: number) {
    const starCountJSON = localStorage.getItem(String(movieId));

    if (!starCountJSON) return 0;
    return Number(JSON.parse(starCountJSON));
}

saveStarRating(movieId: string, starCount: number) {
    localStorage.setItem(movieId, JSON.stringify(starCount));
}

사실 이번 미션의 경우, 영화 상세정보 모달 컴포넌트의 메서드에서 스스로 localStorage를 가져와서 사용하도록 구현했다. 하지만 이 경우엔 사용자의 별점 정보를 로컬스토리지가 아닌 서버에서 데이터를 가져오는 방식으로 변경한다고 했을 때, localStorage를 사용한 컴포넌트의 코드들을 모두 변경해야하는 문제점이 있다. 또한 localStorage는 실제 서버와 연동된 데이터베이스 대신 사용하고 있는 앱 외부의 로컬 저장소이다.

따라서!! 데이터를 저장하고 다루는 로직을 따로 분리하고 여기에 로컬 스토리지와 동기화하는 등의 책임을 맡겨야 한다. 그러면 컴포넌트들은 오로지 인터페이스 부분만 담당할 수 있게 될 것이다.

🎬 UI/UX 개선하기

이번 영화리뷰 미션 2단계의 목표가 UI/UX를 실제 웹앱처럼 쓸 수 있도록 사용성을 개선하는 것인 만큼 이 부분에서 노력을 많이 했다. 그리고 미션 피드백을 받으면서 사용성 개선을 위해 고려해야 하는 부분이 정말 다양하다는 것도 알게 되었고 더 잘 해내고싶다는 마음이 들었다.

영화 리뷰 미션 웹페이지 링크

  • 모바일 검색창 애니메이션
  • 영화아이템 마우스 hover 효과 - 헤더의 박스 쉐도우와 더불어 가장 마음에 든 부분이었다👍
  • 여러 가지 경우에 대한 사용성 개선(스크롤, 대체 이미지 등)
    • 제목이 긴 영화: Night of the Day of the Dawn of the Son of the Bride of the Return of the Revenge of the Terror of the Attack of the Evil, Mutant, Alien, Flesh Eating, Hellbound, Zombified Living Dead Part 2
    • 줄거리가 긴 영화: D-Day 6.6.1944
    • 포스터 이미지와 줄거리가 없는 영화: Let’s protect the common property
    • 검색 결과가 없는 경우
  • 마지막 페이지에 도달하여 스크롤 할 수 없는 경우 화면 하단에 메시지 출력
  • 영화 검색 시 입력값 1~250글자로 제한하기
  • 모달창이 열려있는 경우 뒷배경 스크롤 막기
  • 반응형 레이아웃 구현 시 퍼센트로 그리드 사이즈를 설정하여 동적 그리드 구현 → 스켈레톤UI에서 문제가 발생하게 되는데…
    • 데스크탑: grid-template-columns: repeat(4, calc(80% / 4));
    • 태블릿: grid-template-columns: repeat(3, calc(80% / 3));
    • 모바일: grid-template-columns: repeat(1, 100%);

스크린샷

검색 결과가 없는 경우 & 포스터 이미지가 없는 경우에 나오는 이미지는 페어인 @요술토끼가 파워포인트로 그려준 것이다.

  • 검색 결과가 없는 경우

    검색 결과가 없는 경우
  • 포스터 이미지가 없는 경우

    포스터 이미지가 없는 경우
  • 검색어가 250자 초과인 경우

    검색어가 250자 초과인 경우
  • 더이상 불러올 영화가 없는 경우

    더이상 불러올 영화가 없는 경우

🎬 반응형웹 구현 시 이미지 사이즈를 %로 설정했을 때 스켈레톤UI가 안 보이는 문제에 관하여

문제원인

스켈레톤 UI가 백그라운드 이미지로 들어가있었다. 그런데 동적 그리드를 사용해 반응형웹을 구현하면서, 그리드와 이미지 사이즈를 모두 %로 주었기 때문에 이미지가 로드 되기 전에는 이미지 사이즈가 0%가 되어 백그라운드 이미지인 스켈레톤 UI가 안 보였다.

해결방안

그래서 데스크탑, 태블릿, 모바일별로 이미지 사이즈를 계산해서 최소 이미지 사이즈를 넣어주어 해결했다!

데스크탑 최소 width = 992px
Item-list 사이즈 = 992*0.8 = 793.6px
그리드 컬럼 4개 합한 사이즈 = 793.6*0.8 = 634.88px
영화 포스터 최소 이미지 사이즈 = 634.88/4 = 159px

태블릿 최소 width = 768px
Item-list 사이즈 = 768*0.8 =614.4px
그리드 컬럼 3개 합한 사이즈 = 614.4*0.8 = 491.52px
영화 포스터 최소 이미지 사이즈 = 491.52/3 = 164px

모바일 최소 width = 280px
Item-list 사이즈 = 280*0.8 = 224px
그리드 컬럼 1개 사이즈 = 224px
영화 포스터 최소 이미지 사이즈 = 224px

예를 들어 데스크탑의 경우 최소 너비를 992px로 설정하고 한 행에 4개의 영화를 넣는다고 했을 때, 아래와 같이 영화 포스터의 최소 이미지 사이즈를 계산할 수 있다.

그리드사이즈

영화 목록의 한 행의 너비는 화면 너비의 80%인 992*0.8 = 793.6px

영화 아이템 사이의 갭(grid-column-gap)을 제외한 너비, 즉 그리드 컬럼 4개를 합한 너비는 영화 목록 한 행의 너비의 80%인 793.6*0.8 = 634.88px

그리드 컬럼 1개에 영화 포스터 이미지가 들어가므로 영화 포스터의 최소 이미지 사이즈는 634.88/4 = 159px (px는 정수단위만 가능)

✏️ 방학 중 레벨1을 복습하며 메모한 것들

모든 피드백은 비판적으로 수용하기

사실 처음 코드리뷰를 받을 때부터, 지금까지도 리뷰어가 준 피드백은 무비판적으로 수용하게 될 때가 많다. 그래서 비판적 수용을 위해 의식적으로 다시 한번 생각해보고 피드백을 반영하려고 노력 중이다. 실제로 리뷰어가 제안한 코드가 틀렸을 때도 있었다. 그래서 그것에 대해 다시 질문했고 내 방법이 맞다는 피드백을 받았을 때 비판적 수용에 대한 필요성을 더 느끼게 됐다.

에러메시지 작성 팁 3가지

에러메시지 작성 팁
1. 무슨 일이 일어났는지, 왜 일어났는지 설명해주세요.
2. 다음 단계를 제안해주세요.
3. 알맞은 톤을 사용해주세요.

개발자로서 사용자에게 전달하는 가치가 무엇인지 고민하기

존 카맥은 누군가에게 이러한 고민을 받았는데 다음과 같이 이야기했습니다.
"소프트웨어는 사람들이 무언가를 성취할 수 있게 도와주는 도구일 뿐이라는 사실을 많은 프로그래머가 이해하지 못합니다. 전달하는 가치에 집중하고, 도구의 세부 사항에 매몰되지 마세요"
레벨1 과정 수료를 앞둔 지금, 내가 개발자가 되어 "전달하는 가치"가 무엇인지도 함께 고민해 보는 것은 어떨까요?
(우아한 테크코스 LMS 中)

상속보단 조합을 사용하자

상속은 상위 객체와 하위 객체가 강력하게 의존하기 때문에 캡슐화를 깨뜨리고 확장성을 고려했을 때 좋지않다.

가능하면 순수함수로 작성하자

순수함수란 주어진 입력에 의해서만 출력이 결정되고 부수효과가 없는 함수다.

모든 요소를 동적으로 렌더링할 필요가 있을까?

  • 정적으로 그려줄 부분과 동적으로 변경할 부분을 구분해서 고민해보자.
  • 모든 요소를 동적으로 렌더링한다면 검색엔진최적화에도 좋지 않을 것이다.

    구글 검색의 3단계: 크롤링 → 색인 생성 → 검색 결과 게재
    각 구청 홈페이지의 조직도 정보를 크롤링해본 적이 있는데 동적웹페이지의 경우 어떤 이벤트가 발생하도록(예를 들어 검색 버튼 클릭) 제어해주어야 크롤링이 가능해서 번거로운(?) 면이 있었다.

설계를 잘하기 위해서는 무엇을 해야할까?

  • 문제를 먼저 정의하고(기능 목록 작성), 의미 있는 작은 단위로 쪼개자
  • 도메인영역과 UI영역을 나눠보자
  • 어떤 모듈/객체/함수가 어떤 역할을 해야하는지 정의해보자

컴포넌트란?

  • 내 생각: 애플리케이션을 구성하는 기본 단위로 화면에 보여지는 부분을 각자의 기준에 따라 나눈 것
  • 다른 컴포넌트에 의존하지 않고 재사용이 가능하다면 좋은 컴포넌트, 모듈화가 잘된 컴포넌트라 할 수 있겠다.

우테코의 교육과정은?

모든 배움의 과정에서 물음표를 던지게 만들어준다.

- 왜 이런 기술을 사용하지?
- 어떤 경우에 활용할 수 있지?
- 이 기술의 한계점은 뭘까?
- 어떻게하면 더 잘 사용할 수 있을까?
- 이 앱에서 가장 핵심이 되는 기능은 뭐지?
- 앱 외부의 로컬 저장소인 localStorage는 도메인 영역에 있어야 할까?
- 로컬스토리지가 아니라 실제 서버에서 데이터를 받아오도록 변경한다면 어떻게 대응할 수 있을까?
- 어디까지 테스트해야 의미가 있을까?
- 어디까지 재사용 하는 게 편리할까? 오히려 과하지 않을까?
- 이번 미션에서 얻어갈 것은 뭐지?

또한 이러한 물음에 대하여 자문자답 함으로써,
어떤 의도로 코드를 작성했고, 특정 기술 또는 패턴을 도입 했다면 어떤 필요성을 느껴서 도입 했는지 설명할 수 있게 된다.

profile
Fake It till you make It!

0개의 댓글

관련 채용 정보