[Project] 커스텀 이벤트로 어ㅡ썸하게 상태 변경하기 (Vanilla JS)

young_pallete·2021년 9월 3일
2

no#tation

목록 보기
9/11
post-custom-banner

시작하며 🌈

제가 이 프로젝트를 하면서, 문득 이런 생각을 해봤어요.

뭔가 하나하나 상태변경을 쭉~내려가지 않고, 그냥 변경시킬만한 아이디어가 없을까?!

그러다 생각난 게 바로, customEvent를 잘 활용하자!는 거였어요.
계속해서 window에서 렌더링 된 이후 이벤트 리스너를 듣고 있다가, 어떠한 이벤트가 dispatch될 때 이를 캐치하면 되는 거 아닐까?!라는 생각을 하게 된 거죠!

따라서 이를 바로 실천해보았읍니다!

꽤나 유용한 방법 같아요. vanillaJSSPA를 구현하신다면, 상태 관리, 이렇게 해보는 것도 굉장히 유용한 팁인 듯 합니다! 이 글은, 프로젝트 진행에 대한 점검 차 쓰는 글이자, 그런 분들께 도움이 되기 위해 작성 되었어요.


본론 📖

저는 목표는 다음과 같아요.

title이라는 컴포넌트가 바뀔 때마다, 그 옆에 나오는 sideBar에서의 해당 페이지 title도 같이 변경되어주면 안될까?!

그래서 이를 해결하기 위해서는 컴포넌트 간에 서로 변경할 수 있도록, 글로벌한 상태관리가 필요했어요.

따라서 다음과 같이, customEvent라는 디렉토리를 설정해주었답니다.
그 다음에 다음과 같이 글로벌하게 리스너를 들어주는 dispatcher와 액션을 불러주는 action을 만들어 주었지요!

아, 그런데 이렇게 관리하는 것도 상당히 나중에 커지면 복잡해질 수 있을 거란 생각이 듭니다.
제 생각에 Best는 전역에서 호출될 dispatcher와 이에 대한 event를 구분하는 게 직관적, 체계적인 면에서 더 낫다고는 생각하네요!
(그러나 제 프로젝트는 작은 편이라, 이 정도는 애교로! 넘어가주시죠😆)

customEvent.js

간단하지요?!
그냥 단순하게, 이런 이벤트가 생기면, 디스패치할 dispatcher을 만들어주는 거에요!

그러면 결국 이벤트가 호출되면, 전역적으로 해당되는 곳에서, 호출되면 그만이지요!

저는 좀 더 명확하게 '이 페이지에서 이게 호출됐다'는 걸 명확히 하기 위해, 페이지에다가 설정해주었어요!

import names from '@/utils/classNames';

const DISPATCH_UPDATE_TITLE = 'action/update-title';

export const updateTitleDispatcher = () => {
  const { postLink } = names;
  window.addEventListener(DISPATCH_UPDATE_TITLE, async e => {
    const { id, title } = e.detail;
    document.querySelector(`.${postLink}[data-id="${id}"]`).textContent = title;
  });
};

export const dispatchUpdateTitle = ({ id, title }) => {
  window.dispatchEvent(
    new CustomEvent(DISPATCH_UPDATE_TITLE, {
      detail: {
        id,
        title,
      },
    }),
  );
};

호출 시작!

그럼 이 친구를, 어디에서 호출해야 할까요?!
저는 크게 2가지 경우를 고려했습니다.

  • 페이지에서 debounceonUpdate에다가 넣자!
  • input에서 addEventListener에서 keyup에서 onUpdate를 수행하는 onEdit을 시킬 때, 같이 바로 실행되는 것으로 넣자!

여기서 저는 첫번째를 택했습니다.
왜냐하면 "만약에 수정이 안됐는데?"를 떠올렸기 때문입니다.

유저 입장에서 낙관적 업데이트로 인해 수정이 서버에서는 되지 않았는데, 된 것처럼 나온다면?

결국 이러한 문제에서, UX가 전반적으로 떨어질 것을 우려했기 때문입니다!
(이건 제가 아직 뭘 모르고 있어서 그런 걸 수도 있겠네요.)

여튼, 각설하고, dispatch해주는 애를, 제대로 끼워볼까요?!

PostEditPage.js

import getPost from '@/apis/route/post/getPost';
import getPostList from '@/apis/route/post/getPostList';
import updatePost from '@/apis/route/post/updatePost';
import {
  dispatchUpdateTitle,
  updateTitleDispatcher,
} from '@/utils/customEvent';
import Header from '@/components/Header';
import PostForm from '@/components/PostForm';
import SideBar from '@/components/SideBar';
import names from '@/utils/classNames';
import { _createElemWithAttr } from '@/utils/customDOMMethods';
import debounce from '@/utils/debounce';
import { getItem, setItem } from '@/utils/storage';
/*
 this.state = {
    id: 'new',
    title: '',
    content: '',
    documents: [],
    createdAt: '',
    updatedAt: '',
 }
 */
export default function PostEditPage({
  $target,
  initialState = {
    id: 'new',
    title: '',
    content: '',
    documents: [],
    createdAt: '',
    updatedAt: '',
  },
  onClick,
}) {
  const { mainContainer, postEditPage } = names;
  const $page = _createElemWithAttr('div', [postEditPage]);
  const $container = _createElemWithAttr('div', [mainContainer]);
  $page.appendChild($container);
  this.state = initialState;
  const { id } = this.state;

  const defaultValue = { title: '', content: '' };
  const post = getItem(getLocalPostKey(id), defaultValue);

  /***************************************
   *             components              *
   ***************************************/

  const header = new Header({
    $target: $page,
    initialState: {
      username: this.state.username,
    },
  });
  const sideBar = new SideBar({
    $target: $container,
    initialState,
    onClick,
  });

  const postForm = new PostForm({
    $target: $container,
    initialState: {
      ...post,
    },
    onEdit: debounce(
      post => setItem(getLocalPostKey(this.state.id), { ...post }),
      500,
    ),
    onUpdate: debounce(async ({ title, content }) => {
      await updatePost(this.state.id, this.state.username, {
        title,
        content: content ?? '\n', // 수정의 경우, 제목을 바꿨으나 내용물이 없으면 보내지지 않으므로 띄어쓰기 하나라도 만들어서, 제목을 수정시킵니다.
      });
      dispatchUpdateTitle({
        id: window.location.pathname.split('/')[2],
        title,
      });
    }, 1500),
  });

  // id가 바뀔 때 페이지의 상태가 변화합니다!
  this.setState = async nextState => {
    this.state = {
      ...this.state,
      ...nextState,
    };
    const { id, username } = this.state;
    header.setState({ username });
    sideBar.setState({
      username,
      documents: await getPostList(this.state.username),
    });

    let post = getItem(getLocalPostKey(this.state.id), defaultValue);
    if (id) {
      post = await getPost(id, username);
    }
    postForm.setState(post);
    this.render();
  };

  this.render = () => {
    if ($target.querySelector('form') === null) {
      postForm.render(); // 에디터의 경우 여기서 렌더링을 해줘야, setState할 때 다시 렌더링되지 않습니다.
    }
    $page.appendChild($container);
    $target.appendChild($page);
  };

  updateTitleDispatcher();
}

const getLocalPostKey = postId => {
  return `temp-save-${postId}`;
};

이렇게 끼워주면 간단합니다!
또한 마지막 줄쯤에 보면, dispatcher을 심어줌으로써, 이벤트가 일어날 때 이 페이지에서 들을 수 있도록 잘~ 관리해주었군요!

그렇다면, 결과를 살펴볼까요?!

옆에 사이드바에서 debounce로 설정한 1.5초 이후에 잘 바뀌네요!


마치며 👏

후! 일단 꽤나 어ㅡ썸한 전역 상태 변경 방법을 찾아내서 기분이 정~말 좋아요!
사실... 이로 인해 리팩토링을 할 게 정말 많이 떠올라서 아찔하기는 하지만...

그래도 어쩌겠어요! 제가 가독성 있는 코드를 사랑하는 만큼, 저 역시 이에 대해 책임을 져야하는 걸요 😂😂😂

여튼, 여러모로 오늘 마감인 프로젝트도 나름 완전한 목적은 달성하지를 못했어도, 제 나름대로 의의를 찾아가는 것 같아 만족스럽습니다 👍👍

다들, 즐거운 코딩하새오! 이상 😆😆🌈

profile
People are scared of falling to the bottom but born from there. What they've lost is nth. 😉
post-custom-banner

0개의 댓글