알고 보니 프로젝트 요구사항에 하위 document
도 form
밑에 렌더링되게 해놨던 걸 까먹고 있었습니다 😂
따라서 이를 어떻게 하면 좀 더 재사용성 높게 잘 쓸 수 있을까하다가, 또 리팩토링을 했어요!
결과적으로 어떻게 변했는지를 살펴봅시다!
기존 subDocument
를 렌더링하는 로직은 다음과 같았어요.
export const _renderPosts = ($parentNode, nowDocuments) => {
const {
outlinedIcon,
addIcon,
sz150,
postNextNewText,
postNextNew,
postNextNewIcon,
} = names;
if (!nowDocuments.length) {
const $postNextNew = _createElemWithAttr('div', [postNextNew]);
const $postNewIcon = _createElemWithAttr(
'span',
[outlinedIcon, sz150, postNextNewIcon],
addIcon,
);
const $postNextNewText = _createElemWithAttr(
'span',
[postNextNewText],
'빈 페이지',
);
_appendChilds($postNextNew, $postNewIcon, $postNextNewText);
_appendChilds($parentNode, $postNextNew);
return;
}
nowDocuments.map(doc => {
const { id, title, documents: nextDocs } = doc;
const post = new Post({
$target: $parentNode,
initialState: {
id,
title,
},
});
_renderPosts(post.$postNext, nextDocs);
});
};
나름 잘 짰다고 생각했는데요. 문제가 있었어요.
하위 documents
에서는 Post
라는 컴포넌트에서의 button-box
가 없었으면 좋겠다는 생각을 하게 됐어요!
그렇다면 sideBar
에서의 기능이 좀 더 잘 보이기도 하고, 굳이 편집을 하면서 보지도 않은 하위 document
를 삭제할 이유가 없었기 때문이죠.
뭐, 있다고 하더라도, 나중에
className
과 조건문만 바꾸면 되니, 이런 게 함수를 통한 리팩토링의 장점 아닐까요!
따라서 이를 일단 제 욕심(...)에 따라 없앨 수 있도록 설계를 했답니다.
먼저 isSideBar
라는 매개변수를 받아봅시다.
export const _renderPosts = ($parentNode, nowDocuments, isSidebar = true) => {
const {
outlinedIcon,
addIcon,
sz150,
postNextNewText,
postNextNew,
postNextNewIcon,
} = names;
if (!nowDocuments.length && isSidebar) {
const $postNextNew = _createElemWithAttr('div', [postNextNew]);
const $postNewIcon = _createElemWithAttr(
'span',
[outlinedIcon, sz150, postNextNewIcon],
addIcon,
);
const $postNextNewText = _createElemWithAttr(
'span',
[postNextNewText],
'빈 페이지',
);
_appendChilds($postNextNew, $postNewIcon, $postNextNewText);
_appendChilds($parentNode, $postNextNew);
return;
}
nowDocuments.map(doc => {
const { id, title, documents: nextDocs } = doc;
const post = new Post({
$target: $parentNode,
initialState: {
id,
title,
},
isSidebar,
});
_renderPosts(post.$postNext, nextDocs, isSidebar);
});
};
import names from '@/utils/classNames';
import { _appendChilds, _createElemWithAttr } from '@/utils/customDOMMethods';
/*
id, title
*/
const {
postsItem,
postToggleBtn,
postCreateBtn,
postRemoveBtn,
postLink,
postBlock,
postNext,
outlinedIcon,
sharpIcon,
sz150,
sz175,
addIcon,
arrowRightIcon,
removePostIcon,
} = names;
export default function Post({ $target, initialState }) {
this.state = initialState;
const { id, title } = this.state;
this.$post = _createElemWithAttr('div', [postsItem, postBlock]);
this.$postToggleButton = _createElemWithAttr(
'button',
[postToggleBtn, outlinedIcon, sz175, 'toggle'],
arrowRightIcon,
);
this.$post.dataset['id'] = id;
this.$postLink = _createElemWithAttr('a', [postLink], title);
this.$postLink.dataset['id'] = id;
this.$postCreateButton = _createElemWithAttr(
'button',
[postCreateBtn, outlinedIcon, sz150],
addIcon,
);
this.$postRemoveButton = _createElemWithAttr(
'button',
[postRemoveBtn, sharpIcon, sz150],
removePostIcon,
);
this.$postNext = _createElemWithAttr('section', [postNext]);
this.$postNext.dataset['id'] = id;
_appendChilds(
this.$post, // append 대상
this.$postToggleButton,
this.$postLink,
this.$postCreateButton,
this.$postRemoveButton,
);
this.render = () => {
$target.appendChild(this.$post);
$target.appendChild(this.$postNext);
};
this.render();
}
import names from '@/utils/classNames';
import {
_appendChilds,
_createElemWithAttr,
_renderChild,
} from '@/utils/customDOMMethods';
/*
id, title
*/
export default function Post({ $target, initialState, isSidebar }) {
const {
postsItem,
postToggleBtn,
postButtonBox,
postCreateBtn,
postRemoveBtn,
postLink,
postBlock,
postNext,
sz150,
sz175,
outlinedIcon,
sharpIcon,
arrowRightIcon,
removePostIcon,
addIcon,
} = names;
this.state = initialState;
const { id, title } = this.state;
this.$post = _createElemWithAttr('div', [postsItem, postBlock]);
this.$post.dataset['id'] = id;
this.$postLink = _createElemWithAttr('a', [postLink], title);
this.$postLink.dataset['id'] = id;
this.$postToggleButton = _createElemWithAttr(
'button',
[postToggleBtn, outlinedIcon, sz175, 'toggle'],
arrowRightIcon,
);
if (isSidebar) {
this.$postButtonBox = _createElemWithAttr('div', [postButtonBox]);
this.$postCreateButton = _createElemWithAttr(
'button',
[postCreateBtn, outlinedIcon, sz150],
addIcon,
);
this.$postRemoveButton = _createElemWithAttr(
'button',
[postRemoveBtn, sharpIcon, sz150],
removePostIcon,
);
_appendChilds(
this.$postButtonBox,
this.$postCreateButton,
this.$postRemoveButton,
);
_appendChilds(
this.$post, // append 대상
this.$postToggleButton,
this.$postLink,
this.$postButtonBox,
);
} else {
_appendChilds(
this.$post, // append 대상
this.$postToggleButton,
this.$postLink,
);
}
this.$postNext = _createElemWithAttr('section', [postNext]);
this.$postNext.dataset['id'] = id;
this.render = () => {
_renderChild($target, this.$post, postsItem);
_renderChild($target, this.$postNext, postNext);
};
this.render();
}
일단 기존 코드에서 조건만 해주면 되니, 간단하죠?
다만 주의할 건, 클래스명까지 반복해서 사용하기 때문에 추후
querySelector
에서 앞의 인스턴스 설정에 유의해야겠죠?
그런 다음, subPosts
를 만들어줍시다.
import checkState from '@/utils/checkState';
import names from '@/utils/classNames';
import {
_createElemWithAttr,
_removeAllChildNodes,
_renderChild,
_renderPosts,
} from '@/utils/customDOMMethods';
import { clickPosts, togglePosts } from '@/utils/customEvent';
export default function SubPosts({
$target,
initialState = {
documents: [],
},
onClick,
}) {
this.state = initialState;
const { subPostsBlock } = names;
const $subPosts = _createElemWithAttr('section', [subPostsBlock]);
$target.appendChild($subPosts);
this.setState = nextState => {
if (!checkState(this.state, nextState)) {
_removeAllChildNodes($subPosts);
this.state = nextState;
const { documents } = this.state;
const $fragment = new DocumentFragment();
_renderPosts($fragment, documents, false);
$subPosts.appendChild($fragment);
}
this.render();
};
this.render = () => {
_renderChild($target, $subPosts);
};
togglePosts($subPosts);
clickPosts($subPosts, onClick);
}
그 다음에는, 이제 PostEditPage
에 붙여주면 끝이겠죠?!
...
const subPosts = new SubPosts({
$target: $postEditContainer,
initialState: {
documents: [],
},
onClick,
});
// id가 바뀔 때 페이지의 상태가 변화합니다!
this.setState = async nextState => {
...
subPosts.setState({
documents: this.state.documents,
});
this.render();
};
this.render = () => {
if (!$target.querySelector('form')) {
postForm.render();
subPosts.render();
} // 에디터의 경우 여기서 렌더링을 해줘야, setState할 때 다시 렌더링되지 않습니다.
...
}
원래였으면 새로 만들 작업을, 기존 코드를 재사용함으로써 더욱 쉽고 간편하게 만들 수 있었군요!
그렇다면, 이를 좀 더 활용해볼까요?
우리는 이벤트를 이제, 기존 코드를 활용함으로써 재설계를 해봅시다.
원래는 이렇게, 이벤트 버블링을 통해 큰 컴포넌트에서 조건을 통해 작은 버튼들을 동작시켰어요.
하지만 이 역시 결국 subPosts
에서 중복이 되기 때문에, 우리 이걸 반복해서 사용해볼까요?
$sideBar.addEventListener('click', e => {
if (
!e.target.classList.contains(postsItem) &&
!e.target.classList.contains(postLink)
) {
return;
}
const postId = e.target.closest(`.${postsItem}`).getAttribute(['data-id']);
onClick(postId);
});
$posts.addEventListener('click', e => {
const { target } = e;
if (!target.classList.contains(postToggleBtn, 'post__link')) return;
const closestPostId = target.closest(`.${postBlock}`).dataset.id;
const $nextItem = $posts.querySelector(
`.${postNext}[data-id="${closestPostId}"]`,
);
$nextItem.classList.toggle('invisible');
target.classList.toggle('toggle');
});
export const togglePosts = $target => {
$target.addEventListener('click', e => {
const { postToggleBtn, postBlock, postNext } = names;
const { target } = e;
if (!target.classList.contains(postToggleBtn, 'post__link')) return;
const closestPostId = target.closest(`.${postBlock}`).dataset.id;
const $nextItem = $target.querySelector(
`.${postNext}[data-id="${closestPostId}"]`,
);
$nextItem.classList.toggle('invisible');
target.classList.toggle('toggle');
});
};
export const clickPosts = ($target, onClickCallback) => {
const { postsItem, postLink } = names;
$target.addEventListener('click', e => {
const { classList } = e.target;
if (!classList.contains(postsItem) && !classList.contains(postLink)) return;
const postId = e.target.closest(`.${postsItem}`).getAttribute(['data-id']);
onClickCallback(postId);
});
};
이제는 $target
과 콜백 함수만 있으면 나머지를 수행해주는군요!
그렇다면 나머지 부분들을 처리해봅시다.
SideBar.js
// click page
clickPosts($sideBar, onClick);
//toggle
togglePosts($posts);
SubPost.js
togglePosts($subPosts);
clickPosts($subPosts, onClick);
이렇게 이벤트 역시 리팩토링하며 재사용성을 높일 수 있었어요!
결과적으로, SideBar
역시 기존 코드 대비 약 10%의 길이가 줄었으며, 더욱 이벤트에 대해 선언적으로 느껴지는군요!
흠... 나중에 좀 더 이벤트다!라는 느낌을 주기 위해
on...
으로 이름을 바꿔주든 하면 더 좋을 거 같네요 😂
이제 하위도 만들었으니, 상위를 만들면 되겠네요.
이는 거꾸로 재귀를 돌리면 쉽게 나올 거 같으니, 얼른 찾아뵐 수 있도록 해야겠습니다.
그렇다면, 다들 즐거운 코딩하새오! 😆😆