[TIL] 내일배움캠프 React 과정 2024.04.25

김형빈·2024년 4월 25일
0

내일배움캠프

목록 보기
7/81
post-custom-banner

오늘의 한 일

  • 개인 과제 필수요구사항 구현
  • readme 작성

바닐라JS를 사용한 영화검색 사이트(개인과제)

목표

  1. Github을 통한 프로젝트 관리
  2. 기능별, 컴포넌트로 파일을 작게 나눈다

1. Github을 통한 프로젝트 관리

2. 기능별, 컴포넌트로 파일을 작게 나눈다

  • 좋은 폴더 구조의 장점

    • 코드의 재사용성 유지보수성 및 확장성의 향상
    • 쉬운 코드 이해 난이도(협업 향상)
  • 이번 프로젝트를 위한 설계

    • 이번 프로젝트는 크게 헤더와 영화 카드를 보여주는 Movieview 컴포넌트로 이루어져있다.
    • 헤더에는 사이트의 제목과 영화 검색의 두 가지 컴포넌트로 구성되어 있다.
    • movieView에 사용되는 card 컴포넌트는 추후 서비스 확장시에 다른 곳에도 쓰일 수 있을 것 같아 common으로 분류하였다.
    • core에는 컴포넌트를 그리는 class인 Component와 Component를 관찰하는 observer를 생성하였다.
    • store에서는 영화 목록 데이터를 저장하고, observer를 통해 변화를 관찰하도록 설정하였다.
    • js에서 env 파일을 다루는데 어려움을 겪어 대체 파일인 constants를 생성하고 gitignore에 추가하였다.

적용한 기술

  • UI Component

  • mvc + obsever 패턴

    (출처: https://jin-pro.tistory.com/80)

  • 두 패턴을 적용한 이유

    • 검색 기능에서는 영화 리스트 데이터만 변경하고 싶다!
    • 의도
      • 검색 -> 데이터 변경
    • 실제 구현
      • 검색 -> 데이터 변경 -> 변경된 ModelView 렌더링
    • 이를 막기 위해서 위의 두 패턴을 적용하였다
  • 실제 적용 예시

    • observer.js

       let currentObserver = null;
      
      export const observe = (fn) => {
       	currentObserver = fn;
       	fn();
       	currentObserver = null;
      };
      
      export const observable = (obj) => {
        Object.keys(obj).forEach((key) => {
          let _value = obj[key];
          const observers = new Set();
      
          Object.defineProperty(obj, key, {
            get() {
              if (currentObserver)  observers.add(currentObserver);
              return _value;
            },
      
            set(value) {
              _value = value;
              observers.forEach((fn) => fn());
            },
          });
        });
        return obj;
      };
    • store.js

      import { observable } from "../component/core/observer.js";
      
      export const store = {
      	state: observable({
        		movieList: [],
      	}),
      
       	setState(newState) {
         		for (const [key, value] of Object.entries(newState)) {
          		if (!this.state[key]) continue;
          		this.state[key] = value;
         		}
       	},
      };
    • Component.js

      import { observable, observe } from "./observer.js";
      
      export default class Component {
      state;
      props;
      $el;
      
      constructor($el, props) {
        this.$el = $el;
        this.props = props;
        this.setup();
      }
      
      setup() {
        this.state = observable(this.initState());
        observe(() => {
          this.render();
          this.setEvent();
          this.mounted();
        });
      }
      
      initState() {
        return {};
      }
      template() {
        return "";
      }
      render() {
        this.$el.innerHTML = this.template();
      }
      setEvent() {}
      mounted() {}
      addEvent(eventType, selector, callback) {
        this.$target.addEventListener(eventType, (event) => {
          if (!event.target.closest(selector)) return false;
          callback(event);
        });
      }
      }
    • searchView.js

      import Component from "../../core/Component.js";
      import { store } from "../../../store/store.js";
      import fetchGet from "../../../utils/apis/fetchGet.js";
      import { MOVIE_API_KEY } from "../../../../constants/constants.js";
      
      export default class SearchView extends Component {
      template() {
        return `
            <div id="searchTitle" class="searchTitle">영화 검색:</div>
            <input type="text" id="searchInput" placeholder="영화 제목을 검색해 보세요"" />
            <button id="searchBtn" class="searchBtn">검색</button>
          `;
      }
      async setEvent() {
        const { $el } = this;
        const search = $el.querySelector("#searchInput");
      
        $el.querySelector("#searchBtn").addEventListener("click", () => {
          fetchGet(
            `https://api.themoviedb.org/3/search/movie?api_key=${MOVIE_API_KEY}&language=ko-KR&query=${search.value}&page=1&include_adult=false`
          ).then((res) => {
            store.setState({ movieList: res.results });
          });
        });
      }
      }
    • searhView에서 데이터 변경 -> store 변경(Obseverable) -> obsever에 등록되어 있던 함수 실행 -> Component 렌더링 -> view 업데이트

    • 위 과정을 통해서 searchView에서는 원하던 데로 데이터만 업데이트 할 수 있게 되었다!

오늘의 문제 해결

Card를 반복적으로 Component에 호출하기

문제 해결 1

  • 처음 생각은 cardStore를 만들어서 반복문으로 데이터를 변경하면 변경될 때 컴포넌트를 화면에 렌더링 하는 방식을 생각하였다
  • 그러나 card가 상태가 필요한 컴포넌트일까?
  • 상태란?
    • UI에 동적으로 표현되는 데이터
    • 변화하는 데이터
  • card는 생성된 이후에 데이터가 변할 일이 없다. 그렇다면 store를 생성하고 관리할 필요가 없다는 뜻이고 card를 렌더링 하는 다른 방법을 찾아야 한다는 뜻이기도 하다.

문제 해결 2

  • 그렇다면 props로 데이터를 넘겨서 마운팅하자
export default class MovieView extends Component {
  template() {
    return `
    <div id="movieContainer" class="movieContainer"></div>  
      `;
  }
  mounted() {
    const { $el } = this;

    store.state.movieList.forEach(
      ({ poster_path, original_title, overview, vote_average, id }) => {
        const $view = $el.querySelector(`#movieContainer`);
        new Card($view, {
          src: poster_path,
          title: original_title,
          content: overview,
          rating: vote_average,
          id: id,
        });
      }
    );
  }
}
  • 그럼데...
  • 카드가 하나만 떴다...
  • 범인은 Component.js
 render() {
    this.$el.innerHTML = this.template();
  }
  • 계속해서 movieContainer의 innerHTML을 교체만 하고 있었던 것이다

문제 해결 3

export default class MovieView extends Component {
  template() {
    return `
    <div id="movieContainer" class="movieContainer">
    ${store.state.movieList
      .map(
        ({ id }) => `
        <div class="movieCard-${id}" id="movieCard-${id}"></div>
      `
      )
      .join("")}
      </div>  
      `;
  }
  mounted() {
    const { $el } = this;

    store.state.movieList.forEach(
      ({ poster_path, original_title, overview, vote_average, id }) => {
        const $view = $el.querySelector(`#movieCard-${id}`);
        new Card($view, {
          src: poster_path,
          title: original_title,
          content: overview,
          rating: vote_average,
          id: id,
        });
      }
    );
  }
}
  • 이번에는 movieContainer에 movieCard-id라는 div를 추가하고 해당 div의 innerHTML을 교체하는 방식으로 수정했다.
  • 카드가 잘 나온다!

오늘의 회고

오늘은 어제 운동할 때 피곤하다 생각 들더니 아침부터 지각을 하고 말았다.. 분명히 알람도 맞춰두고 핸드폰도 무음에서 진동으로 바꿨는데... 전화까지 못 듣고 푹 자버렸다. 그래도 푹 자니까 아침 컨디션이 좋긴 했다. 뒤늦게나마 알고리즘 수업을 듣고, 어제에 이어서 개인 과제를 진행했다. 어제 걱정했던거와 달리 observer패턴과 UI Component를 적용하면서 성공적으로 필수요구사항을 모두 구현할 수 있었다. 적용하는데에 있어 어려움이 있었지만 적용하고 나니 특히 검색 기능에서 두 패턴을 사용하는 이유를 체감할 수 있었다.
필수요구사항을 끝내고 나니 심리적 여유가 생겼고, 가장 먼저 README를 작성했다


내가 작성한 README
과거에 본 README 중 잘 썼다고 생각한 걸 참고했지만 쓰다 보니까 이게 가장 좋은 README 방식인가? 라는 생각도 든다. 다른 사람들은 README를 어떻게 쓰는지 조금 더 찾아봐야겠다.

그리고 아직 내일까지 시간이 있으니 남은 시간 동안 fetch와 async/await 중 어떤 것이 좋은 지에 대한 고민, 예외처리 (img src가 없는 경우, 검색에 해당하는 카드가 없는 경우 등), 느린 이미지 로딩을 해결할 수 있는 방법, 선택요구사항(검색 입력란에 커서 자동 위치 시키기, 키보드 enter 입력시 검색 되게 하기)을 추가하기, 마음에 안 드는 css 수정하기를 하려한다. 써보니 생각보다 많기는 한데 할 수 있는데 까지 한번 해보자!
profile
The secret of getting ahead is getting started.
post-custom-banner

0개의 댓글