연습 또 연습/js 연습문제

순수JS로 라우팅, 컴포넌트, 상태관리 리팩토링

furaha 2023. 9. 27. 15:50
반응형

JS와 파이어베이스를 활용해서 프로필 등록관리하는 사이트를 만들었다.

그런데 ES6 클래스 문법 강의를 듣고 리액트를 사용하지 않고, 순수 JS로 컴포넌트, 상태관리, 라우터를 구현하는 것을 보고

리팩토링하면 리액트 선수학습에 도움이 될 거라 생각해서 약 1주일간 리팩토링 작업에 돌입했다!

 

파일구조 전후

 

기존) js 폴더 안에 페이지 기준으로 파일들이 있었음

이후) js 폴더 안에 components, core, routes, store로 각 기능별로 나누었음

 

제일 먼저 만든 파일은 core.js 파일이다.

이 파일에는 총 2개의 클래스와 1개의 함수를 내보내고 있다.

 

1) Component 클래스는 돔요소를 생성하고 렌더링한다. 또한 props와 state 인자를 받아 Component의 속성으로 정의한다.

2) createRouter 클래스는 인자값으로 {경로,컴포넌트} 객체를 담은 배열을 받고 경로에 맞는 컴포넌트의 돔요소들을 보여준다.

3) Store 클래스는 state 로 등록된 데이터가 바뀔 때마다 콜백함수들을 실행해주는 역할을 한다.

 

 

위 클래스들을 확장해서 페이지별로 컴포넌트를 생성했고

index.js 에서 경로와 각 컴포넌트를 createRouter 매개변수로 넣어주었다.

 

 

그 다음 store.js를 구현했다.

Store라는 객체를 만들고, 이 객체는 상태관리를 할 데이터들을 넣어준다.

초기값은 일단 비워두고,

getData라는 비동기 함수를 사용해서 파베에서 데이터를 가져오고

그 데이터를 store 상태값에 넣어준다.

 

 

다음으로는 컴포넌트로 분리할 영역을 정했다

검색영역과, 리스트의 각 아이템을 컴포넌트로 분리해 주었다.

SearchItem.js 와 ProfileItem.js 

ProfileItem.js 에서는 store에 있는 상태데이터들을 가져와서 map으로 돌려 렌더링을 해준다.

 

  async render()  {
    
    let profileList = '';

    try{
      const { urlId } = await getData();
      const { members } = profileStore.state;

      members.forEach((doc, index) => {
        const counselingType = counselingTypes[doc.sort];
        const btnClass = counselingType ? counselingType.className : '';

        profileList += `
          <a href="#/detail?id=${urlId[index]}" class="list-item card">
			태그들 생략
          </a>
        `;
      });

    } catch(error) {
      console.log(error)
    }

  }

 

CRUD 기능 중 Read를 제외하고는, Delete, Update, Write 이 3가지는 상태관리를 하지는 못했다.

바로 파이어베이스의 컬렉션들을 업데이트해주었다. (추후에 나머지 Revision 기능들까지 state로 관리가 되게끔 해 볼 예정이다.)

 


 

머리를 쥐어 싸맨 그간의 오류들

Q. 함수에서 두 개의 값을 리턴하려면? 그리고 리턴값을 두 개로 내보내면 어떻게 또 받지? 

하는 너무나 기초적인 질문이 떠올랐다.

 

export const getData = async () => {

  //urlId 변수 생성
  //dataArray 변수 생성

  return {urlId, dataArray};
  
}

 

이렇게 {} 객체 기호 안에 내보낼 두 요소를 넣어주면 된다.

그리고 받을 때는 구조분해할당으로 받아와서 사용하면 된다.

 

      const {urlId, dataArray} = await getData();

 


 

+ 새롭게 알게 된 것 :  forEach에 index 인자를 넣을 수 있다는 것! index 인자를 굉장히 유용하게 활용했다.

 

 

      dataArray.forEach((doc, index) => {
        const counselingType = counselingTypes[doc.sort];
        const btnClass = counselingType ? counselingType.className : '';

        profileList += `
          <a href="#/detail?id=${urlId[index]}" class="list-item card">
            <input type="checkbox" name="selection" value="${doc.name}" onclick="${checkSelectAll()}"/>
            <p class="card-img-wrap">
              <img src="${doc.image}" class="card-img-top" />
            </p>
          </a>
        `;
      });

 


 

정말 최대의 고비가 무엇이었냐,,

각 컴포넌트의 render() 안에 적은 스크립트가 새로고침을 해야지만이 작동한다.

분명 core.js 에서 this.render()로 무조건 렌더링이 되게끔 했는데 왜일까

 

Detail.js:68 Uncaught (in promise) TypeError: Cannot set properties of null (setting 'innerHTML')
    at ProfileDetail.render (Detail.js:68:28)

 

이 타입에러를 6시간 동안 200번은 본 것 같다ㅜ

 

문제 되는 스크립트가 아래와 같은데

this.el.innerHTML로 돔 요소를 생성해 주었다. 

그리고 그 안에 감싸고 있는 detail-wrap이라는 요소를 선택해야 그 아래 나머지 스크립트들이 먹히는데

detail-wrap을 계속 못 찾고 null 값이어서 실행이 되지 않았다.

 

import { Component } from "../core/core.js";
import profileStore from "../store/store.js"
import { getData } from "../store/store.js";

export default class ProfileDetail extends Component {

  constructor() {
    super();
  }

  async render(){

    this.el.innerHTML = /* html */`
    <main>
    <div class="container" id="detail-wrap"></div>
    <div class="text-center">
      <button class="btn btn-light" onclick="location.href='/'">목록으로</button>
      <button class="btn btn-primary" id="update-btn">수정</button>
      <button class="btn btn-danger" id="complete-btn">저장</button>
    </div>
  </main>
    `
    
    const detailWrap = document.getElementById('detail-wrap');
    let detailView = '';
   
   생략...

 

원인은 HTML이 렌더링 되기 전에 render() 함수가 실행되기 때문이다.

비동기적으로 코드가 실행되고 있는 것 같다. 그래서 HTML 해당 돔 요소를 찾으려고 하니까 못 찾는 것이다.

해결로는 document.getElementById 가 아닌 this.el.getElementById로 바꿔주면 된다.

그러니까 document에서 찾지 말고 해당 컴포넌트 안에서 찾으라고 하면 당연히 돔 요소를 찾게 된다.

 

혹은 document 에 DOMContentLoaded 이벤트를 걸어주고 그 안에 detail-wrap 요소를 찾으면 된다.

그러기엔 코드가 늘어나기 때문에, 선택자를 바꿔주는 방법을 선택했다.

 

대부분이 이 선택자에 관련된 오류였고, 코드의 순서를 보장하기 위해서 사용하는 async await의 용도를 더욱 되새길 수 있었다.

 


 

 

그러면 이벤트를 걸을 경우에도 this.el로 선택자를 바꿔주어야 하는 것 아닌가?

 

 

아니다! 일반함수에서 this는 이벤트를 일으키는 대상이 this 가 되기 때문에 

컴포넌트를 가리키지 못해서 원하는 이벤트 실행이 잘 안 된다.

그래서 내가 의도한 대로this를 가리키게 하려면 화살표 함수로 바꿔주면 된다.

 

signBtn.addEventListener('click', function () {
document.querySelector('#sign-area').style.display = 'block';
document.querySelector('#login-area').style.display = 'none';
});

 

이렇게 말이다.

 

signBtn.addEventListener('click', () => {
  this.el.querySelector('#sign-area').style.display = 'block';
  this.el.querySelector('#login-area').style.display = 'none';
});

 

 

 

일반함수와 화살표 함수의 this 쓰임새가 다른 것은 확실히 이해했다ㅎㅎ

 


 

리팩토링하면서 느낀 점

공통된 컴포넌트는 여러 개인데,데이터에 따라서 달라지는 텍스트, 컬러, 버튼 유무 등등 UI 가 많이 달라지다 보니까

데이터를 부모로부터 관리하는 것이 필요하다는 것을 절실히 느끼고 리팩토링을 시작했다.

 

좋았던 점은

데이터를 상위 개념에서 관리하고,데이터를 write 할 때는 구조분해로 불러오는 것을 많이 연습할 수 있었다.

그 과정에서 데이터가 바뀌면 그 자식들이 렌더링 되게 구현했는데 이 개념이 결국 리액트에서는 useState의 개념인 것 같다.

 

그리고 라우터로 쪼개다 보니까 굉장히 에러가 많이 났는데, 다 선택자의 문제였다.

document 이냐 this.el 이냐 또한 this는 또 화살표함수냐 일반함수냐에 따라서 오류가 많이 났었다.

정적 페이지로만 작업할 때는 다 document로 선택자를 잡기 때문에 전혀 이런 고민을 하는 상황이 없었는데

이번 기회를 통해서 확실히 렌더링이 언제 되는지 그리고 코드의 실행 순서가 어떻게 알고 있는 것이 중요하다고 느꼈다.

 

그리고 이건 저번에도 느낀 거지만 then 은 바로 다음 코드 동작하니까, 데이터를 불러올 때는 async await로 불러올 것

그리고 항상 에러 예외처리 해줄 것!!!

 

store와 파베랑 crud 같이 섞으려니까 너무 어려웠다,,

store를 사용하는 것은 '읽기'만 했다.

나머지 revision 기능들을 완벽 구현해보고 싶다.

반응형