컴포넌트 의존성 분리

2024. 4. 4. 19:40프로젝트/미션으로 배우는 순수 자바스크립트

🤔문제 인식하기

// main.js
import TodoInput from './TodoInput.js';
import TodoList from './TodoList.js';

try {
  const app = document.querySelector('#app');
  let state = [
    {
      text: 'JS 공부하기',
      isCompleted: false,
    },
    {
      text: '알고리즘 공부하기',
      isCompleted: false,
    },
  ];
  const todoList = new TodoList(app, state);
  const todoInput = new TodoInput(app, todoList);
} catch ({ message }) {
  alert(message);
}

main.js 코드이다. TodoList 컴포넌트와 TodoInput 컴포넌트를 생성하고 있다.

그런데 TodoInput 컴포넌트의 생성자로 TodoList 컴포넌트를 받고 있다.

 

// TodoInput.js
export default class TodoInput {
  #target;
  constructor(target, todoList) {
    this.#target = target;
    const input = document.createElement('input');
    input.setAttribute('type', 'text');
    this.#target.appendChild(input);

    input.addEventListener('keyup', (e) => {
      if (e.key === 'Enter') {
        const text = e.target.value;
        // 이 부분에서 외부 컴포넌트의 상태를 직접적으로 변경하고 있다.
        const newState = [...todoList.getState(), { text, isCompleted: false }];
        todoList.setState(newState);
        e.target.value = '';
      }
    });
  }
}

TodoInput 컴포넌트에서 constructor로 전달받은 todoList의 외부 메서드를 통해 todoList의 상태를 변경하고 있다.

이렇게 되면 TodoInput 컴포넌트와 TodoList 컴포넌트 간의 의존성이 높아지게 된다.

예를 들어 TodoList의 외부 메서드인 setState의 이름이나 파라미터가 변경되게되면 TodoInput 컴포넌트의 코드도 변경된다.

의존성을 아예 배제하는 것이 아닌 최소한의 의존성을 갖도록 수정해야한다.

또한 TodoInput 컴포넌트는 단일 책임의 명령을 가져야한다. TodoInput은 입력값을 입력만 받으면 되는 컴포넌트이다.

이 입력값이 어떻게 가공되고 어디에 붙여지는지는 몰라도 된다는 것이다.

 

우리가 변경해야할 부분

  • 외부 컴포넌트를 constructor로 받아와서 내부에서 변경하는 코드를 없앤다.
  • TodoInput은 입력값을 받기만 하면 된다.

🎯해결해보기

export default class TodoInput {
  #target;
  constructor(target, onInput) {
    this.#target = target;
    const input = document.createElement('input');
    input.setAttribute('type', 'text');
    this.#target.appendChild(input);

    input.addEventListener('keyup', (e) => {
      if (e.key === 'Enter') {
        const text = e.target.value;
        onInput(text);
        e.target.value = '';
      }
    });
  }
}

TodoInput 컴포넌트는 이제 TodoList 컴포넌트를 constructor를 통해 직접적으로 받아오지 않을 것이다.

그리고 input에 이벤트가 발생하면 input에 적은 텍스트 데이터를 받아서 onInput 이라는 콜백함수의 파라미터로 전달한다.

 

import TodoInput from './TodoInput.js';
import TodoList from './TodoList.js';

try {
  const app = document.querySelector('#app');
  let state = [
    {
      text: 'JS 공부하기',
      isCompleted: false,
    },
    {
      text: '알고리즘 공부하기',
      isCompleted: false,
    },
  ];
  const todoList = new TodoList(app, state);
  const todoInput = new TodoInput(app, (text) => {
    const newState = [...todoList.getState(), { text, isCompleted: false }];
    todoList.setState(newState);
  });
} catch ({ message }) {
  alert(message);
}

그렇다면 main.js 코드 내에서 콜백함수의 인자로 들어온 text를 가지고 새로운 데이터를 만들어 TodoList 컴포넌트의 state를 변경하면 된다.

 

이렇게 하면 TodoList와 TodoInput 두 컴포넌트는 서로의 존재를 모르게된다. 즉, 의존성이 완전히 분리되었다.

그리고 TodoInput 컴포넌트 입장에서도 입력받은 text로 무엇을 하는지 모른다. TodoInput 컴포넌트의 역할은 인풋값을 받아오는 것에만 집중을 하게된다. 이 인풋값을 가공하는 것은 TodoInput 컴포넌트를 사용하는 main.js 쪽에서 구현하면 되는 것이다.


컴포넌트 의존성 분리와는 관계없지만 Form 태그를 활용해서 인풋값을 받아오도록 리팩토링한 코드이다.

Form은 submit 이벤트가 발생했을 때 기본동작으로 다른 웹페이지로의 이동이 포함되어 새로고침이 발생한다.

따라서 e.preventDefault() 메서드를 활용해서 기본동작을 막는다.

그리고 Form 내부요소인 input을 선택자로 선택하는 코드도 눈여겨보면 좋다.

export default class TodoInput {
  #target;
  #element;
  constructor(target, onInput) {
    this.#target = target;
    this.#element = document.createElement('form');
    this.#element.innerHTML = `
    <input type='text'/>
    <button type='submit'>Add</button>
    `;
    this.#target.appendChild(this.#element);

    this.#element.addEventListener('submit', (e) => {
      e.preventDefault();
      const input = this.#element.querySelector('input');
      const { value } = input;
      onInput(value);
      input.value = '';
      input.focus();
    });
  }
}