티스토리 뷰

728x90
SMALL

옵저버패턴이란

컴포넌트와 라우터까지 완성을 했다. 이제 바닐라 자바스크립트로 프로젝트를 시작할 수 있을까??
가능은 하다. 그런데 유저 닉네임 값을 여러곳에서 이용한다면 계속해서 서버와 통신을 해야할까?? 너무 비효율적이다. 그렇지 않다면 상위 컴포넌트에서 하위 컴포넌트로 데이터를 넘겨줘야 한다. depth가 작으면 문제가 없지만 재사용성을 고려하면서 만든 컴포넌트는 단위가 작고 몇단계씩 데이터를 넘겨야한다. 그렇게되면 의존성도 높아진다. 모듈간에 의존성이 높은 것은 좋은 현상이 아니다. 그래서 웹 프론트엔드에서는 옵저버패턴을 사용한다. 리덕스도 이 패턴을 사용한다.
옵저버패턴에는 구독(subscribe)과 발행(publish) 개념이 존재한다. 어떤일이 발생했을 때 그 일을 알려달라고 하는 것이 구독이다. 특별히 다른 용어가 아니라 실제로 우리가 사용하는 용어다. 유튜브라던가... 유튜브라던가...
이제 필요한 역할을 생각해보면

  1. 구독방법을 포함
  2. 구독리스트를 담아야 함
  3. 이벤트를 발행하는 방법을 포함

대충 이렇게 된다.

일반적인 옵저버패턴 코드

class Observable {
  constructor() {
    this.observers = new Set();
  }

  subscribe(observer) {
    this.observers.add(observer);
  }

  unsubscribe(observer) {
    this.observers = [...this.observers].filter(subscriber => subscriber !== observer);
  }

  notify() {
    this.observers.forEach(observer => observer());
  }
}

정말 간단하다. 그냥 subscribe 메서드에 콜백함수를 인자로 넣는다. 그리고 observers라는 값에 추가한다. 자료구조는 set이든 객체 리터럴이든 상황에 맞게 선택하면 된다. 그리고 notify로 모든 observers에 있는 콜백함수들을 실행시키면 된다. 엄청 간단하면서도 많은 도움이 되는 패턴이다. 참고로 notify에 data를 파라미터로 넣고 그 값을 콜백함수에 넣을 수도 있다. 활용하기 나름이다..

컴포넌트에 적용하기

이제 저번에 만든 컴포넌트에 적용해보자. 물론 상위 옵저버 클래스도 수정해야된다.

basic-observable.js

class Observable {
  constructor() {
    this.observers = new Set();
  }

  setState(nextState) {
    this.state = { ...this.state, ...nextState };
    this.notify();
  }

  subscribe(observer) {
    observer();
    this.observers.add(observer);
  }

  unsubscribe(observer) {
    this.observers = [...this.observers].filter(subscriber => subscriber !== observer);
  }

  unsubscribeAll() {
    this.observers = new Set();
  }

  notify() {
    this.observers.forEach(observer => observer());
  }
}

export default Observable;

거의 비슷한데 그냥 state만 추가를 했다.

이제 방금 만든 클래스를 상속하는 저장소를 만들자. 리덕스도 그렇고 store라는 이름을 많이들 사용한다.
간단한 자동로그인, 로그인, 로그아웃 메서드만 구현했다.

user-store.js

import axios from 'axios';
import Observable from '../lib/basic-observable';
import { checkAuthApi } from '../utils/api/auth';

class UserStore extends Observable {
  constructor() {
    super();
    this.state = { user: null };
  }

  async autoLogin() {
    try {
      const {
        data: { user },
      } = await checkAuthApi();
      this.login(user);
    } catch (err) {
      if (!axios.isAxiosError(err)) {
        console.log('내부 에러');
      }
    }
  }

  async login(user) {
    this.setState({ user });
  }

  async logout() {
    this.setState({ user: null });
  }
}

export default new UserStore();

그리고 엔틀포인트에서 자동로그인을 해준다.

  init() {
    if (localStorage.getItem('user')) userStore.autoLogin();
  }

그리고 헤더에서 보통 로그인 상태를 보여줘서 예시 코드를 적어두겠다.

header.js

import axios from 'axios';
import Component from '../../../lib/component';
import userStore from '../../../store/user-store';
import './style.css';
import { logoutApi } from '../../../utils/api/auth';

class Header extends Component {
  setup() {
    this.state = { user: undefined };
  }

  markup() {
    const { user } = this.state;
    return /* html */ `
    <header class="header">
      <div class="empty">empty</div>
      <h1>
        <a href="/">M's blog</a>
      </h1>
      ${user ? '<button class="logout">로그아웃</button>' : '<a href="/login">로그인</a>'}
    </header>
    `;
  }

  componentDidMount() {
    userStore.subscribe(() => this.setState({ user: userStore.state.user }));
  }

  setEvent() {
    this.addEvent('click', '.logout', () => {
      this.logout();
    });
  }

  async logout() {
    try {
      await logoutApi();
      localStorage.removeItem('user');
      userStore.logout();
    } catch (err) {
      if (axios.isAxiosError(err)) {
        // TODO: winston으로 기록?
        console.log(err);
      } else {
        console.log('내부 에러');
      }
    }
  }
}

export default Header;

리덕스는??

지금은 사실 옵저버패턴을 그냥 적용한 것이고 리덕스같은 라이브러리를 만든것은 아니다. 추후에 리덕스 같은 라이브러리를 만들고 블로그에 올리겠다...

참조글 : 루카스 크롱님 글

728x90
LIST
댓글
공지사항