0509 - 0515

0512

Vanilla JavaScript로 웹 컴포넌트 만들기

최근에는 바닐라 자바스크립트만을 사용하여 기업들의 과제 전형을 두 차례 진행했다.
그 중 한 기업에서는 기존에 사용하던 방식대로 controller, store, render로 파일을 구분하여 기능을 구현하였는데, 이번 기회에는 새로운 도전 방식으로 구현해보고자 도전하게 되었다.
방식은 프로그래머스 사전 과제의 고양이 사진첩 만들기와 황준일 개발자님의 블로그를 참고하였다.

컴포넌트와 상태 관리

브라우저와 자바스크립트가 발전하는 과정에서 아예 브라우저 단에서 렌더링을 하고, 서버에서는 REST API 혹은 GraphQL 같이 브라우저 렌더링에 필요한 데이터만 제공하는 형태로 기술이 변화했다.

이제는 직접적으로 DOM을 다루는 행위가 급격하게 감소했고, 상태를 기준으로 DOM을 렌더링하는 형태로 발전한 것이다. DOM이 변하는 경우가 상태에 종속 되어버린 것이다. 반대로 말하면, State가 변하지 않을 경우 DOM이 변하면 안 되는 것이다. 이러한 과정 속에서 CSR이라는 개념과 상태 관리라는 개념이 생기게 되었다.

SSR을 사용하는 시절에는 서버에서 HTML을 만들어서 클라이언트에 넘겨주었기에 브라우저에서는 굳이 데이터의 깊은 단계까지, 정교하게 관리할 필요가 없었다. 그러나 브라우저 단에서 렌더링을 하기 위해선, 렌더링에 필요한 상태를 정교하게 관리해야 한다. 그래서 Redux 같은 상태관리 라이브러리가 생겨났다.

Entry Point

시작점은 index.js에서부터 시작한다.

import App from "./components/App";

new App(document.querySelector("#app"));

엔트리 포인트에서는 상위 부모 컨테이너 역할을 할 App 컴포넌트를 불러와서 생성하고, 인자로 id가 app인 DOM을 넘긴다.

컴포넌트 추상화

새로운 패턴의 JS 개발을 보며 리액트와 코드가 상당히 닮아있다는 생각을 했다.
이전과는 다르게 클래스 문법을 사용해서 컴포넌트를 구현하였고, 공통되는 코드 로직을 Component 클래스로 추상화하고, 이를 위임받아 사용하도록 작성했다.

export default class Component {
  constructor($target, props) {
    this.$target = $target;
    this.props = props;

    this.setUp();
    this.setEvent();
    this.render();
  }

  setUp() {}

  template() {
    return "";
  }

  render() {
    this.$target.innerHTML = this.template();

    this.mounted();
  }

  mounted() {}

  setEvent() {}

  setState(newState) {
    this.state = { ...this.state, ...newState };

    this.render();
  }
}
  • 모든 컴포넌트는 해당 컴포넌트를 렌더링 할 상위 컴포넌트와 상위 컴포넌트로부터 전달받을 props를 파라미터로 받는다.
  • constructor에서는 전달받은 파라미터를 인스턴스의 값으로 저장하고, setUp, setEvent, render 메서드를 호출한다.
  • setUp에서는 상태 초기화를 담당한다.
  • render에서는 파라미터로 전달받은 target에 template에 정의한 값을 innerHTML로 할당하고, mounted 메서드를 호출한다. mounted에서는 하위의 컴포넌트를 불러와서 생성하는 코드를 작성한다.
  • setEvent에서는 이벤트 위임을 활용하여 이벤트 핸들러를 구현한다.
  • setState로 상태가 변경되면 다시 리렌더링이 발생하는 구조이다. 여기서 state는 setState로만 변경해야 한다.

사용 예시

import Component from "./Component";

export default class Modal extends Component {
  template() {
    const { showModal } = this.props;

    return `
      ${
        showModal
          ? `
            <div class="modal-content">
              <h2>강의 추가</h2>
              <div class="title">
                <label for="course-title-input">강의 제목</label>
                <input
                  type="text"
                  id="course-title-input"
                  class="course-title-input"
                  placeholder="강의 제목을 입력해주세요"
                  autocomplete="off"
                />
              </div>
              <div class="price">
                <label for="course-price-input">강의 가격</label>
                <input
                  type="number"
                  id="course-price-input"
                  class="course-price-input"
                  placeholder="강의 가격을 입력해주세요"
                  step="1000"
                  autocomplete="off"
                />
              </div>
              <div class="btn-group">
                <button type="button" class="cancel-btn">취소</button>
                <button type="button" class="confirm-btn">확인</button>
              </div>
            </div>
            <div class="dim"></div>`
          : ""
      }
    `;
  }

  setEvent() {
    const { closeModal, addCourse } = this.props;

    this.$target.addEventListener("click", (e) => {
      if (e.target.classList.contains("cancel-btn")) closeModal();
      else if (e.target.classList.contains("confirm-btn")) {
        const title = document
          .querySelector(".course-title-input")
          .value.trim();
        const price = document.querySelector(".course-price-input").value;

        if (title === "") {
          alertErrorMessage("강의 제목을 입력해주세요.");
          return;
        }
        if (price === "" || +price < 0) {
          alertErrorMessage("강의 가격에는 0 이상의 숫자만 입력해주세요.");
          return;
        }

        addCourse(title, price);
      }
    });
  }
}
  • 위의 컴포넌트를 사용한 예시이다. template 메서드를 확장하여 사용하면 자동으로 render 메서드가 실행되고, 불필요한 코드를 반복해서 사용해야할 필요가 사라진다.




0513

GraphQL

기존 Rest API 통신은 크게 두 가지 문제점을 가지고 있다.

over fetching

  • 필요한 것보다 더 많은 데이터를 가져온 경우
  • 예를 들어, 영화 API로 데이터를 불러와서 그 중 제목 데이터만 사용하는 경우 나머지 정보는 필요 없게 되는데 이를 오버 패칭이라고 함
  • GraphQL은 쿼리 랭귀지이기 때문에 정확하게 필요한 정보만 요청할 수 있어서, 필요없는 데이터는 받지않고 정확히 필요한 정보만 요청할 수 있다.

under fetching

  • 필요한 것보다 적은 양의 데이터를 가져온 경우
  • 영화 API에서 곧 개봉하는 영화와 현재 상영중인 영화 데이터를 받아와야 할 경우 REST를 사용한다면 2개의 각기 다른 URL로 요청을 보내야 한다. 이는 로딩 타임 증가로 이어진다. GraphQL을 사용하면 1개의 쿼리로 2개의 정보를 한 번에 요청할 수 있다.

먼저 특정 기능을 위해 여러번 api가 호출되고, 특정 요청에 적절한 응답을 돌려주기 위해서는 api를 새로 만들어야 합니다. 그리고 api를 유지보수하기 어렵다는 문제점이 있습니다. 즉, 관리해야 할 endpoint의 증가로 인하여 발생되는 문제점입니다.

기존의 rest api에서 정보를 얻기 위해 여러번 네트워크를 호출하거나 ,다양한 api를 호출해야 했습니다.
하지만 graphql은 단 하나의 endpoint를 제공하며, 단 한 번의 요청으로 모든 정보를 가져옵니다.

장단점

장점

  • 오버 패칭과 언더 패칭의 문제를 개선해준다.
  • HTTP 요청 횟수와, 응답 사이즈를 줄일 수 있음

단점

  • http 캐싱이 REST보다 복잡함
  • 파일 전송 등 Text만으로 하기 힘든 작업들을 처리하기 복잡함
  • 고정된 요청과 응답만 필요할 경우 Query로 인해 요청의 크기가 REST API보다 커짐

사용하기 좋은 경우

GraphQL

  • 서로 다른 모양의 다양한 요청들에 대해 응답할 수 있어야 할 때
  • 대부분의 요청이 CRUD에 해당할 때

REST API

  • 캐싱 기능을 잘 사용하고 싶을 때
  • 파일 전송 등 단순한 Text로 처리되지 않는 요청들이 있을 때
  • 요청의 구조가 명확하게 정해져 있을 때




0514

최근에 디자인 패턴 공부에 대한 중요성을 느끼고 나서, 제대로 정리해보고자 개인 스터디를 시작하게 되었다.

스터디 내용은 Refactoring guru의 링크에 있는 정리 내용을 보고 학습할 계획이다.
물론 모든 패턴 내용을 처음부터 끝까지 다 살펴보면 제일 좋겠지만, 전부터 궁금했던 패턴들 우선적으로 살펴볼 예정이다.
이번 스터디가 아키텍처를 설계하는 데에 있어 많은 도움이 되기를 희망한다.


디자인 패턴이란?

디자인 패턴은 소프트웨어 디자인에서 일반적으로 발생하는 문제들에 대한 일반적인 해결책이다.
이는 코드에서 반복되는 디자인 문제를 해결하기 위해 사용자 지정할 수 있는 청사진과도 같다.

패턴은 특정 코드 조각이 아니라 특정 문제를 해결하기 위한 일반적인 개념이다. 패턴 세부 사항을 따르고 자신의 프로그램의 현실에 맞는 솔루션을 구현할 수 있다.

알고리즘과 디자인 패턴은 모두 일반적인 솔루션을 설명하기 때문에 종종 혼동된다. 하지만 알고리즘은 항상 어떤 목표에 달성할 수 있는 명확한 일련의 작업을 의미하지만, 패턴은 솔루션에 대한 보다 높은 수준의 설명이다. 두 개의 다른 프로그램에 적용된 동일한 패턴의 코드는 다를 수 있다.

알고리즘을 비유하자면 요리법이라고 할 수 있다. 반면, 패턴은 청사진에 가깝다. 결과와 특징이 무엇인지 확인할 수 있지만, 정확한 구현 순서는 사용자에게 달려있다.

패턴의 역사

패턴은 객체 지향 설계의 일반적인 문제에 대한 일반적인 솔루션이다.
다양한 프로젝트에서 솔루션이 계속 반복되면 누군가는 결국 이름을 지정하고 솔루션을 설명한다. 이것이 기본적으로 패턴이 발견되는 방법이다.

패턴을 배워야 하는 이유

  • 디자인 패턴은 소프트웨어 디자인의 일반적인 문제에 대한 시도되고 테스트된 솔루션의 툴킷이다. 이러한 문제가 발생하지 않더라도 패턴을 아는 것은 객체 지향 설계 원칙을 사용하여 모든 종류의 문제를 해결하는 방법을 가르쳐주기 때문에 여전히 유용하다.
  • 디자인 패턴은 팀원이 보다 효율적으로 의사 소통하는 데 사용할 수 있는 공통 언어를 정의한다. 예를 들어, 패턴의 이름을 안다면 싱글톤이 무엇인지 설명할 필요가 없다.

패턴의 분류

디자인 패턴은 복잡성, 세부 수준 및 디자인 중인 전체 시스템에 대한 적용 범위에 따라 다르다.

가장 기본적이고 낮은 수준의 패턴은 종종 idioms(관용구)라고 불리는데 이들은 보통 하나의 프로그래밍 언어에만 적용된다.
가장 보편적이고 높은 수준의 패턴은 architectural patterns(아키텍처 패턴)이다. 개발자는 이러한 패턴을 대부분의 언어로 구현할 수 있다. 다른 패턴과 달리 전체 애플리케이션 아키텍처를 설계하는 데 사용될 수 있다.

또한 모든 패턴은 의도 또는 목적에 따라 분류될 수 있다. 이 책은 세 가지 패턴의 주요 그룹을 다룬다.

  • Creational patterns(생성 패턴)은 기존 코드의 유연성과 재사용성을 증가시키는 객체 생성 메커니즘을 제공한다.
  • Structural patterns(구조적 패턴)은 구조를 유연하고 효율적으로 유지하면서 객체와 클래스를 더 큰 구조로 조립하는 방법을 설명한다.
  • Behavioral patterns(행동 패턴)은 효과적인 의사소통과 객체 간의 책임 할당을 처리한다.




0515

Observer 패턴

의도

옵저버 패턴은 관찰 중인 객체에서 어떤 이벤트가 발생했을 때, 다른 객체들에게 알리는 구독 메커니즘을 정의하는 행동 디자인 패턴이다. 779B15B2-81D3-49E1-9720-9035C1B62355

문제점

고객과 상점 두 가지 형태의 객체가 존재한다고 상상해보자. 고객은 곧 매장에 출시될 특정 브랜드의 제품에 매우 관심이 있다.
고객은 매일 매장을 방문하여 제품 재고를 확인할 수 있다. 그러나 제품이 아직 운송 중일 때의 매장 방문은 대부분 무의미하다.

F83B7D10-C75E-4D8D-82E4-0AF1F45A80B3

반면에 상점은 새 제품이 출시될 때마다 모든 고객에게 수많은 이메일(스팸으로 간주될 수 있는)을 보낼 수 있다. 이것은 매일 방문하는 고객에게는 도움이 될 수 있지만, 새로운 제품에 관심이 없는 다른 고객들을 화나게 할 것이다.

위와 같은 방법은 고객이 제품 가용성을 확인하는 데 시간을 낭비하거나 상점이 잘못된 고객에게 알리는 등 리소스를 낭비하게 된다.

해결책

퍼블리셔(publisher) - 관심있는 상태 객체이자, 상태가 변경되었을 때 다른 객체에게 알리는 객체
구독자(subscriber) - 퍼블리셔의 상태 변화를 추적하기 원하는 모든 객체

옵저버 패턴은 퍼블리셔 클래스에 구독 메커니즘을 추가하여 개별 객체가 해당 퍼블리셔로부터 오는 이벤트를 구독하거나 구독 취소할 수 있도록 제안한다. 실제로 이 메커니즘은 구독자 객체에 대한 참조 목록을 저장하기 위한 배열 필드와 해당 목록에 구독자를 추가하거나 제거할 수 있는 여러 공용 메서드로 구성된다.

29D1F7F2-EDD8-4CD1-A34A-7697C5D93AD9
이후 퍼블리셔에게 중요한 이벤트가 발생할 때마다 구독자를 통해 객체에 대한 특정 알림 메서드를 호출한다.

실제 앱에는 동일한 퍼블리셔 클래스의 이벤트를 추적하는 데 관심이 있는 수십 개의 서로 다른 구독자 클래스가 있을 수 있다.
퍼블리셔를 이러한 모든 클래스에 연결하고 싶지는 않을 것이다. 그리고 퍼블리셔 클래스가 다른 사람들에 의해 사용되어야 한다면 그들 중 일부에 대해 미리 알지 못할 수도 있다.

그렇기 때문에 모든 구독자가 동일한 인터페이스를 구현하고 게시자가 해당 인터페이스를 통해서만 구독자와 통신하는 것이 중요하다.
이 인터페이스는 퍼블리셔가 알림과 함께 일부 컨텍스트 데이터를 전달하는 데 사용할 수 있는 매개변수 집합과 함께 알림 메서드를 선언해야 한다.

8D6EBA99-22D2-4286-991B-F2C1DD8332FD
앱에 여러 유형의 퍼블리셔가 있고 구독자가 모든 퍼블리셔와 호환되도록 하려면 모든 퍼블리셔가 동일한 인터페이스를 따르도록 할 수 있다. 이 인터페이스는 몇 가지 구독 방법만 설명하면 된다. 이 인터페이스를 통해 구독자는 구체적인 클래스에 연결하지 않고도 게시자의 상태를 관찰할 수 있다.

실세계와 유사한 사례

808D0CB5-B9E2-4B39-BA38-DD1F3C5D32AB

  • 신문이나 잡지를 구독하면 더 이상 다음 호가 있는지 확인하기 위해 상점에 갈 필요가 없다. 대신 발행인은 발행 직후 우편함으로 직접 보낸다.
  • 발행인은 구독자 목록을 유지 관리하고 관심 있는 잡지를 알고 있다. 구독자는 발행인이 새 잡지 호를 보내는 것을 중단하려는 경우 언제든지 목록을 떠날 수 있다.

구조

AF509362-4D0C-41F3-9B2F-8E8EDD5C8FCC

  1. 퍼블리셔는 다른 객체에 대한 관심 이벤트를 발행한다. 이러한 이벤트는 퍼블리셔가 상태를 변경하거나 일부 동작을 실행할 때 발생한다. 퍼블리셔에는 새 구독자가 가입하고 나갈 수 있도록 하는 구독 인프라가 포함되어 있다.
  2. 새 이벤트가 발생하면 퍼블리셔는 구독 목록을 살펴보고 각 구독자 객체의 구독자 인터페이스에 선언된 알림 메서드를 호출한다.
  3. 구독자 인터페이스는 알림 인터페이스를 선언한다. 대부분의 경우 단일 update 방법으로 구성된다. 메서드에는 퍼블리셔가 업데이트와 함께 일부 이벤트 세부 정보를 전달할 수 있도록 하는 여러 매개 변수가 있을 수 있다.
  4. 구체적 구독자는 발행한 알림에 대한 응답으로 몇 가지 작업을 수행한다.
  5. 일반적으로 구독자는 업데이트를 올바르게 처리하기 위해 몇 가지 컨텍스트 정보가 필요하다. 이러한 이유로 퍼블리셔는 종종 일부 컨텍스트 데이터를 알림 메서드의 인수로 전달한다.
  6. 클라이언트는 게시자 및 구독자 객체를 별도로 생성한 다음 퍼블리셔 업데이트를 위해 구독자를 등록한다.

적용 가능성

  • 한 객체의 상태가 변경되면 다른 객체도 변경해야 할 수 있고, 실제 객체 집합을 미리 알 수 없거나 동적으로 변경되는 경우 옵저버 패턴을 사용한다.
  • 앱의 일부 객체가 제한된 시간 동안 또는 특정 경우에만 다른 객체를 관찰해야 하는 경우 패턴을 사용한다.
  • 예시로 리덕스에서도 옵저버 패턴을 활용하여 스토어 상태가 변경되면 구독 함수를 실행한다.

장단점

장점

  • 개방/폐쇄 원칙 - 퍼블리셔의 코드를 변경하지 않고도 새 구독자 클래스를 도입할 수 있다.
  • 런타임에 객체 간의 관계를 설정할 수 있다.

단점

  • 구독자는 무작위 순서로 알림을 받는다.

타입스크립트로 패턴 구현

/**
 * Subject 인터페이스는 구독 관리를 위한 메서드를 정의한다.
 */
interface Subject {
  attach(observer: Observer): void;

  detach(observer: Observer): void;

  notify(): void;
}

/**
 * Observer 인터페이스는 Subject에서 특정 이벤트가 발생하면 호출할 업데이트 메서드를 정의한다.
 * update 함수에서 subject의 상태를 활용할 수도 있기 때문에 인자로 전달한다.
 */
interface Observer {
  update(subject: Subject): void;
}

class ConcreteSubject implements Subject {
  public state = 0;
  private observers: Observer[] = [];

  public attach(observer: Observer): void {
    const isExist = this.observers.includes(observer);
    if (isExist) {
      return console.log("Subject: Observer has been attached already.");
    }

    console.log("Subject: Attached an observer.");
    this.observers.push(observer);
  }

  public detach(observer: Observer): void {
    const observerIndex = this.observers.indexOf(observer);
    if (observerIndex === -1) {
      return console.log("Subject: Nonexistent observer.");
    }

    this.observers.splice(observerIndex, 1);
    console.log("Subject: Detached an observer.");
  }

  public notify(): void {
    console.log("Subject: Notifying observers...");
    for (const observer of this.observers) {
      observer.update(this);
    }
  }

  public someBusinessLogic(): void {
    console.log("\nSubject: I'm doing something important.");
    this.state = Math.floor(Math.random() * (10 + 1));

    console.log(`Subject: My state has just changed to: ${this.state}`);
    this.notify();
  }
}

class ConcreteObserverA implements Observer {
  public update(subject: Subject): void {
    if (subject instanceof ConcreteSubject && subject.state < 3) {
      console.log("ConcreteObserverA: Reacted to the event.");
    }
  }
}

class ConcreteObserverB implements Observer {
  public update(subject: Subject): void {
    if (
      subject instanceof ConcreteSubject &&
      (subject.state === 0 || subject.state >= 2)
    ) {
      console.log("ConcreteObserverB: Reacted to the event.");
    }
  }
}

const subject = new ConcreteSubject();

const observer1 = new ConcreteObserverA();
subject.attach(observer1);

const observer2 = new ConcreteObserverB();
subject.attach(observer2);

subject.someBusinessLogic();
subject.someBusinessLogic();

subject.detach(observer2);

subject.someBusinessLogic();

실행 결과

C75281B1-E7CC-44D0-8E5C-0DA97A1E5D3E

태그:

카테고리:

업데이트: