[JS] Form Data 처리하기

왜 이렇게 했을까.. 생각하여 작성한 뻘글입니다.

🚨 정보 제공보다, 작성자의 생각 경로를 기록한 게시글입니다.
확장성 있는 컴포넌트 작성하기 => Form input 데이터 처리하기로 제목을 바꾼 게시글입니다.

계기 : 바닐라 JS 과제 제출

개발 챌린지 2차 전형인 과제 제출 단계를 하면서,
반복되는 컴포넌트에 대해 확장성 있게 작성하라고 요구되어 있었습니다.

시간 제한이 있는 시험에서 그런 숙련도가 부족했기에 오래 생각하고 만들 수가 없었습니다.

단순히 dom에 접근하는게 아닌, 재사용 가능한 컴포넌트를 만들라고 하였기에
기능은 구현했지만, 컴포넌트로써는 의미 없이 구색 맞추기로 제출하게 되었습니다.

때문에, 어떻게 확장성있는 컴포넌트를 만들 수 있을까 고민하게 되었습니다.

React

컴포넌트는 React를 배울 때 많이 듣는 개념입니다.

React 자체가 굉장히 어렵다고 생각 했을 땐, 컴포넌트 자체가 무슨 얘기인지 몰랐지만,
재사용이 가능한 독립된 모듈을 말합니다.

컴포넌트는 작은 h1태그 하나일 수도 있습니다.
이 또한, 여러번 재사용 할 수 있습니다.

이러한 독립된 덩어리를 여러번 사용해 리스트를 만들거나, 조합하여 복잡한 UI를 만들 수 있습니다.

또, React는 컴포넌트가 상태를 가지게 하고 상태가 변경되면 자동으로 Rerendering됩니다.
심지어 Render 될 부분을 HTML과 유사한 JSX로 만들기 때문에 간단하고 직관적입니다.

컴포넌트 만들기

목표

Filter 컴포넌트 만들기
영화를 필터에 따라서 검색할 수 있도록 합니다.
[예시]

나중에 다양한 필터를 추가할 수 있도록, 확장성 있는 컴포넌트로 만듭니다.

첫 시도

시험을 볼 때, 컴포넌트 = HTML 덩어리에 집중했습니다.

접근방법

지금까지 반복적이며, 추가될 가능성이 있는 데이터를 화면에 나타내려고 할 때,
forEach를 써서 각 요소를 통해 Element를 만들고, 문서에 추가하도록 해왔습니다.

const movieList = document.querySelector('#moive-list');

// API를 통해 받아온 데이터라고 가정합니다.
const movies = [
  {
    title : 'iron man1',
    year : 2008,
  },
  {
    title : 'spider man1',
    year : 2002,
  },
]

function paintMovie({title,year}) {
  const movieElement = document.createElement('div');
  const titleElement = document.createElement('h1');
  titleElement.textContent = title;
  const yearElement = document.createElement('span');
  yearElement.textContent = year;

  movieElement.append(titleElement,yearElement);
  movieList.append(movieElement);
}

movies.forEach(paintMovie)

뭐 이런식입니다.

리액트를 함께 공부하다보니 리액트에서는 이런 데이터를 map으로
React Component List를 만들고, 화면에 보여주기 때문에, Vanilla JS에서도 저런식으로 구현해왔습니다.

그러나, Vanila JS는 React처럼 간단하게 HTML 덩어리를 만들고, 삽입할 수 없습니다.

movie의 요소가 복잡하다면, paintMovie는 끝 없이 길어집니다.
반복되는 부분을 함수로 줄이려고 해봤지만, 할때마다 이게 맞는건가 생각해왔습니다.

이것 뿐 아니라, 여러 구현에 대해 이렇게 하는게 맞는건가..
와 같은 생각이 늘 제동을 일으킵니다.

구현

우선 필터 항목 확장을 대비해야 하기 때문에, Filter라는 생성자 함수를 만들며 생각했습니다.

React Component는 어떤 역할을 했더라?
Filter로 만들 데이터를 만들고, new Filter를 하여 필터 Element를 만들어야겠군..!

필터로 만들 데이터들.. (벌써 이상함)

const filterContentList = [
    {
        type : 'text',
        name : 'title',
        content : ['제목'],
    },
    {
        type : 'checkbox',
        name : 'day',
        content : ['월','화','수','목','금','토','일'],
    },
    {
        type : 'radio',
        name : 'genre',
        content : ['스릴러','유머','스포츠','일상']
    },
]

Filter 생성자 함수 :
content 배열만큼 input요소를 만들고, this.dom에 담습니다.

function Filter({type = 'input',name='',content=[]} = {}) {
    this.dom = document.createElement('div');

    for (const v of content) {
        const input = document.createElement('input');
        input.type = type;
        input.name = name;

        if (type == 'text') {
            this.dom.append(`${v} : `,input)
        }
        else {
            input.value = v;
            this.dom.append(input,v);
        }
    }
}

이렇게 Filter에서 dom을 만들면, 순회하여 리스트에 넣습니다.
filterContentList에 데이터가 Element가 되어 리스트에 삽입됩니다.

function renderFilter() {
   filterContentList
    .map(filter=> new Filter(filter)).
    .forEach(filter => filterList.append(filter.dom));
}

이것도 나름 장고 끝에 만들었습니다.
시간 제한이 있는 시험에선, 이것조차 정리도 안된 코드로 제출하였습니다.

문제점

1. 사용성이 없습니다.
Filter라는 생성자가 Elemenet를 생성하는 역할 뿐 입니다.
필요한 DOM을 매번 생성하는 것과 다를 바 없습니다.

2. 확장성도 없습니다.
새로운 filter 항목이 생긴다면, 정적인 filterContentList에 추가해야 하는데,
어차피 정적인 데이터는 차라리 HTML에 input을 추가하는게 낫습니다.
거기다 만약, filter가 다른 구성이라면 삽입하는 코드를 바꿔야합니다.

즉, 문제에서 '이렇게 하는게 아닙니다' 라고 말하던,
HTML로 만들고, 단순히 모든 DOM에 접근해서 결과를 얻어내는 것 만도 못합니다.

위에 구현한 것은, HTML로 만들면 되는 정적 Element를
굳이 JS로 각각 동적으로 생성한 것에 지나지 않습니다.

거기다 필터의 구성이 여러가지라서 복잡한 조건들을 통해 만들고 있습니다.

어떻게 해결하지

컴포넌트를 만들려고 할 때, 너무 React component와 비슷하게 하려고 생각하였습니다.

컴포넌트를 만들라고 하니,
데이터를 가지고 DOM을 생성하고, 삽입하고, 내부를 관리하고...

VanillaJS로 구현한다면
반복되는 요소를 만들 때, 매번 요소를 생성하고 삽입하는 작업이
그것에 특화된 React보다 불편합니다.

⭐ 거기다 정적 데이터라면, 그냥 HTML에 추가하는게 당연합니다.

해야할 것은, HTML 필터가 몇 개가 추가되던, 필터를 조작하면 그 데이터를 관리하면 됩니다.

해결

구상

filter가 몇 개가 추가되더라도, 각 필터들의 선택 혹은 입력된 값이 무엇인지 알면 됩니다.
따라서 모든 input이 change될 때, 입력 값을 저장하고, 관리하는 class를 만들었습니다.
모든 input에 핸들러를 할당하는 대신 이벤트위임을 활용하여, 다음과 같이 만듭니다.

class Filter{
  constructor(elem) {
    this._elem = elem;
    elem.addEventListener('change',onChange);
  }
  
  onChange = () => {
    //...?
  }
}

하던 도중 생각났습니다.
어차피 form 내부 데이터가 필요한거면, form이 알지 않나..?

FormData

최근에는 ajax를 활용하여 페이지의 일부만 갱신하지만,
원래 form은 action 경로를 통해, 내부 input의 입력 값을 전송했습니다.

당연히도, form 내부의 선택한 값들을 가지고 있습니다.

new FormData(filterForm)을 통해, 해당 데이터를 얻을 수 있습니다.

function formatQueryString(formData) {
    let queryString = '?';

    for (const [key,value] of formData.entries()) {
        queryString += `${key}=${value}`;
    }

    return queryString
}

class Filter{
    constructor(form) {
        this.form = form;
        form.addEventListener('submit', this.onSubmit);
    }

    onSubmit = event => {
        event.preventDefault();

        const formData = new FormData(this.form);
    
        const queryString = formatQueryString(formData);
        loadMovies(queryString);
    }

}

이렇게 하면, 바로 form 데이터로 queryString을 만들고, 요청할 수 있습니다.
필터가 추가되도 괜찮습니다.

결론

확실한건 Form에서 데이터를 얻는건 매우 간단한 내용인데,
컴포넌트로 만드라는게 뭐지, 확장성을 어떻게 갖지, radio에서 checked인걸 어떻게 가져오더라 등 기본기 부족으로 인해 뻘짓만 한 것 같습니다.

Filter를 컴포넌트라고 할 수 있는건지, 문제에서 말한 확장성이 저게 맞는지
그건 여전히 모르겠지만, 확실한건 2가지 배웠습니다.

  1. 정적인 Element를 추가한다면 당연히 HTML에 추가하면 된다.
  2. 기본기나 제대로 쌓자

좋은 웹페이지 즐겨찾기