제가 이 프로젝트를 하면서, 문득 이런 생각을 해봤어요.
뭔가 하나하나 상태변경을 쭉~내려가지 않고, 그냥 변경시킬만한 아이디어가 없을까?!
그러다 생각난 게 바로, customEvent
를 잘 활용하자!는 거였어요.
계속해서 window
에서 렌더링 된 이후 이벤트 리스너를 듣고 있다가, 어떠한 이벤트가 dispatch
될 때 이를 캐치하면 되는 거 아닐까?!라는 생각을 하게 된 거죠!
따라서 이를 바로 실천해보았읍니다!
꽤나 유용한 방법 같아요.
vanillaJS
로SPA
를 구현하신다면, 상태 관리, 이렇게 해보는 것도 굉장히 유용한 팁인 듯 합니다! 이 글은, 프로젝트 진행에 대한 점검 차 쓰는 글이자, 그런 분들께 도움이 되기 위해 작성 되었어요.
저는 목표는 다음과 같아요.
title
이라는 컴포넌트가 바뀔 때마다, 그 옆에 나오는sideBar
에서의 해당 페이지title
도 같이 변경되어주면 안될까?!
그래서 이를 해결하기 위해서는 컴포넌트 간에 서로 변경할 수 있도록, 글로벌한 상태관리가 필요했어요.
따라서 다음과 같이, customEvent
라는 디렉토리를 설정해주었답니다.
그 다음에 다음과 같이 글로벌하게 리스너를 들어주는 dispatcher
와 액션을 불러주는 action
을 만들어 주었지요!
아, 그런데 이렇게 관리하는 것도 상당히 나중에 커지면 복잡해질 수 있을 거란 생각이 듭니다.
제 생각에 Best는 전역에서 호출될dispatcher
와 이에 대한event
를 구분하는 게 직관적, 체계적인 면에서 더 낫다고는 생각하네요!
(그러나 제 프로젝트는 작은 편이라, 이 정도는 애교로! 넘어가주시죠😆)
간단하지요?!
그냥 단순하게, 이런 이벤트가 생기면, 디스패치할 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가지 경우를 고려했습니다.
- 페이지에서
debounce
된onUpdate
에다가 넣자!input
에서addEventListener
에서keyup
에서onUpdate
를 수행하는onEdit
을 시킬 때, 같이 바로 실행되는 것으로 넣자!
여기서 저는 첫번째를 택했습니다.
왜냐하면 "만약에 수정이 안됐는데?"를 떠올렸기 때문입니다.
유저 입장에서 낙관적 업데이트로 인해 수정이 서버에서는 되지 않았는데, 된 것처럼 나온다면?
결국 이러한 문제에서, UX가 전반적으로 떨어질 것을 우려했기 때문입니다!
(이건 제가 아직 뭘 모르고 있어서 그런 걸 수도 있겠네요.)
여튼, 각설하고, dispatch
해주는 애를, 제대로 끼워볼까요?!
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초 이후에 잘 바뀌네요!
후! 일단 꽤나 어ㅡ썸한 전역 상태 변경 방법을 찾아내서 기분이 정~말 좋아요!
사실... 이로 인해 리팩토링을 할 게 정말 많이 떠올라서 아찔하기는 하지만...
그래도 어쩌겠어요! 제가 가독성 있는 코드를 사랑하는 만큼, 저 역시 이에 대해 책임을 져야하는 걸요 😂😂😂
여튼, 여러모로 오늘 마감인 프로젝트도 나름 완전한 목적은 달성하지를 못했어도, 제 나름대로 의의를 찾아가는 것 같아 만족스럽습니다 👍👍
다들, 즐거운 코딩하새오! 이상 😆😆🌈