[VanillaJS] 2022 Dev-Matching(상반기)

jun5e00·2022년 9월 1일
0

VanillaJS

목록 보기
1/2
post-thumbnail

0. 들어가며

올해 하반기에 Dev-Matching이 있길래 처음으로 지원해보기 위해서 바닐라 자바스크립트 공부를 하기 위해서 이전 문제들을 풀어보는 시간을 가졌다. 그냥 풀기만 하고 넘어가면 기억에 남지 않을 것 같아서 기록으로 남기기로 했다. 과제 명세 다음 링크에 있다.

1. 설계

1-1. View

우선 View 단위로 쪼개서 개발하기로 생각했다. 나는 View 단위로 쪼개서 개발하는걸 좋아하는데, 아무래도 React에서 컴포넌트 기반으로 개발하던 습관 때문이라고 생각한다.

SelectedLanguagesView

API 응답들 중 선택된 항목을 보여주는 부분이다.

SearchInputView

언어를 입력해서 API 요청을 보내게 되는 부분이다.

SuggestionView

API 응답을 보여주는 부분이다.

1-2. Model

전체적인 data를 관리할 부분이다. View에서 사용하는 값들은 Model에서 꺼내서 사용할 예정이다. 전체적으로 private으로 선언하여 getter, setter로 값들을 관리한다.

searchResults

SuggestionView에서 사용할 검색 결과 data

selectedLanguages

SelectedLanguagesView에서 사용할 선택된 언어 목록 data

selectedIndex

SearchResults에서 선택된 결과의 index data (검색 결과에서의 순서)

1-3. Controller

View와 Model을 관리하는 Controller이다. Controller에서 모든 event와 상태 관리가 이루어진다.

2. 구현

2-1. utils

api.js

fetch를 활용하여 API 요청을 보낸다. cache 객체를 활용해서 url을 기준으로 응답 값을 캐싱한다.
해당 과제에서는 API 응답 값이 업데이트 될 수 없기 때문에 값을 최신화할 필요가 없어서 cache를 파기하지 않고 유지해주었다.

const cache = {};

const request = async (url) => {
  if (cache[url]) {
    return cache[url];
  }

  const res = await fetch(url);

  if (res.ok) {
    const json = await res.json();
    cache[url] = json;
    return json;
  }

  throw new Error("요청에 실패함");
};

dom.js

DOM 조작이 필요한 경우에 매번 document.querySelector를 작성하는 부분이 귀찮고 코드가 길다고 생각했다. 따라서 DOM Element를 표현하는 컨벤션인 $를 활용해 dom selector 함수를 만들었다.

const $ = (selector) => document.querySelector(selector);

const $$ = (selector) => document.querySelectorAll(selector);

debounce.js

SearchInput에서 입력한 값이 변하는 즉시 서버에 요청을 보내면 서버에 검색하고자 하지 않은 값을 요청으로 보내게 된다.
따라서 입력이 일정 시간 이루어지지 않으면 서버에 요청을 보낼 수 있게 debounce 함수를 만들었다.

const debounce = (callback, delay = 500) => {
  let timer = null;

  return function (...args) {
    clearInterval(timer);
    timer = setTimeout(() => {
      callback.apply(this, args);
    }, delay);
  };
};

export default debounce;

2-2. View

전체적인 구조는 Controller에서 각 View에 연결할 dom target을 전달한다. View는 해당 target을 받고, target에 HTML을 렌더해준다.

class SomeView {
	#target = null;
    	
	constructor($target) {
    	this.#target = $target;
        
        this.render();
    }
    
    render() {
    	this.#target.insertAdjacentHTML('beforeend', `some template code`)
    }
}

2-3. Model

각 View에 필요한 data를 private으로 저장하여, 외부에서 발생할 수 있는 side effect를 줄이고자 하였다. 따라서 각 data에 접근할 수 있는 getter와 값을 변경할 수 있는 setter를 만들었다.

class Model {
  #searchResults = [];
  #selectedLanguages = [];
  #selectedIndex = 0;

  get searchResults() {
    return this.#searchResults;
  }

  get selectedLanguages() {
    return this.#selectedLanguages;
  }

  get selectedIndex() {
    return this.#selectedIndex;
  }

  set searchResults(search) {
    this.#searchResults = search;
  }

  set selectedLanguages(selectedLanguages) {
    this.#selectedLanguages = selectedLanguages;
  }

  set selectedIndex(selectedIndex) {
    this.#selectedIndex = selectedIndex;
  }
}

export default Model;

2-4. Controller

Controller의 역할은 다음과 같다.

  1. View에 필요한 data를 Model에서 꺼내서 전달한다.
  2. View에서 이벤트를 통해 Model의 값을 변경해야 한다면, View에서 변경된 값을 Model에 업데이트 한다.

addEvent

event를 trigger 하기 위한 함수이다. 함수명을 통해 이벤트를 추가하는 것을 더 명시적으로 보여주고자 만든 함수다.
코드를 다시 보며 생각해 보니 굳이 만들지 않아도 됐을 함수인 것 같다. 그저 addEventListener 대신 addEvent를 사용한 것이라고 생각한다.

addEvent(selector, eventType, callback) {
 selector.addEventListener(eventType, (e) => callback(e));
}

onChangeSearch

SearchInputView에 사용되는 event이다. keyup으로 키 값을 입력 받으면 무시해야 하는 키 값이 아니라면 서버로부터 data를 받아와서 Model에 저장하고 SuggestionView에 반영한다.

onChangeSearch(selector) {
  this.addEvent(
    selector,
    "keyup",
    debounce(async (e) => {
      const { value: keyword } = e.target;

	  if (actionIgnoreKeys.includes(e.key)) return;

      if (!keyword) {
        // keyword가 없다면 검색 결과를 비워주고 선택된 index도 초기화 한다.
        // 그리고 변경된 값들을 SuggestionView에 반영한다.
      }

      // 서버에 data를 요청하고 model과 view에 반영한다.
    })
  );
}

onChangeSelectedIndex

SearchResultView에서 키보드 이동으로 언어를 선택할 수 있는 이벤트 핸들러이다.
Window 전역에 이벤트를 걸어서 어디에 있더라도 키 값을 받을 수 있게 처리했다.
키 값이 Enter인 경우에는 선택된 index의 language를 Model에 저장하고 SelectedLanguagesView에 반영한다.
만약 키 값이 ArrowUp 또는 ArrowDown이라면 Model의 SelectedIndex 값을 바꾸고, SuggestionView에 반영한다.

onChangeSelectedIndex(selector) {
	this.addEvent(selector, "keyup", (e) => {
    	if (e.key === "Enter") {
        	// Model의 searchResults에서 해당하는 언어를 selectedLanguages에 반영한다. 
            
            return;
        }
        
        // 무시해야 하는 키 값이라면 함수를 종료한다.
        
        // 검색 결과가 없다면 함수를 종료한다.
        
        if (e.key === "ArrowUp") {
        	// Model의 selectedIndex 값 처리를 해준다.
        }
        
        if (e.key === "ArrowDonw") {
        	// Model의 selectedIndex 값 처리를 해준다.
        }
        
        // SuggestionView에 반영한다
	}
}

onClickLanguage

검색 결과에 click 이벤트가 발생하면 Model의 selectedLanguages에 반영하고, SelectedLanguagesView에 반영한다.

onSubmit

엔터 입력시 submit 이벤트가 발생하여 기본 동작으로 화면이 리렌더링 되기 때문에 이를 방지하기 위한 이벤트 핸들러 함수이다.

onSubmit(selector) {
    this.addEvent(selector, "submit", (e) => {
      e.preventDefault();
    });
}

마무리

함수를 여러개 작성하여 화면을 관리하고 로직을 작성하는 방식으로 구현하면 복잡하다고 생각했다. 따라서 역할에 맞게 나누어 코드를 작성하고자 하였다. 이를 위해 나름대로의 MVC 정의를 통해서 구현했다.

내가 정의한 MVC 패턴

View와 Model은 서로의 존재를 모르게하고, Controller에서 모든걸 처리할 수 있게 한다.

아쉬운 점

위와 같이 구현하면 Controller에서 모든 로직을 처리하기 때문에 Controller가 비대해진다.
따라서 Controller를 View에서 필요한 로직에 맞게 구분한다면 더 좋겠다고 생각했다.
혹은 React스럽게 작성하는 방법도 있다고 생각했다.

내가 생각한 React스러운 vanillaJS

간단하게 생각해보자면 Component 단위로 View를 쪼개고, 가장 상위에서 state, setState를 만들고 연결된 하위 컴포넌트들에게 state를 전달하고 로직이 처리 되는 방법이다.

profile
공부 일기장

0개의 댓글