예... 현재 12시간 프로젝트 중인데, 같은 부분에서 3시간 삽질했네요...!
인내심이 끝까지 달리던 순간, 뭔가 제가 오해가 있을 거란 부분을 생각해서 수정한 결과, 정상 동작을 하게 됐읍니다!!! 👏👏👏
이래서 정말... 언러닝이 중요하다는 것을 다시금 느낍니다.
그렇다면 어떻게 구현했는지 살펴보시죠!
저는 모달을 만들고 싶었어요. 아무래도 생성하거나 삭제할 때 충분한 정보 전달에도 용이하기도 하고, 함부로 api가 사용되지 않게 함에도 제격이니까요.
따라서! 다음과 같은 모달을 만들어주었습니다.
이 모달은 말이죠, 현재 input
을 넣을 수 있는 모달과, 그렇지 않은 모달을 통합해서 사용하고 있어요. 따라서 isInput
이라는 props
를 받았는데, 이는 불변의 상태로 유지되니 initialState
에 굳이 넣지 않았습니다.
import names from '../../utils/classNames.js';
import {
_appendChilds,
_createElemWithAttr,
} from '../../utils/customDOMMethods.js';
import Input from './Input.js';
export default function Modal({
$target = document.querySelector('#app'),
head = '내용을 입력해주세요!',
isInput = false,
initialState = { title: '' },
onConform,
}) {
const {
modalBlock,
container,
modalConformButton,
modalCancelButton,
modalHead,
modalInput,
modalButtonBox,
} = names;
const $fragment = new DocumentFragment();
this.$container = _createElemWithAttr('div', [container]);
const $modal = _createElemWithAttr('div', [modalBlock]);
const $modalHead = _createElemWithAttr('h3', [modalHead], head);
const $modalButtonBox = _createElemWithAttr('div', [modalButtonBox]);
const $conformButton = _createElemWithAttr('button', [modalConformButton]);
const $cancelButton = _createElemWithAttr('button', [modalCancelButton]);
$conformButton.textContent = '확인';
$cancelButton.textContent = '취소';
$fragment.appendChild(this.$container);
this.$container.appendChild($modal);
$modal.appendChild($modalHead);
if (isInput) {
this.state = initialState;
const input = new Input({
$target: $modal,
placeholder: '제목을 입력해주세요!',
initialState: this.state,
onChange: title => {
this.setState({ title });
},
});
input.$input.classList.add(modalInput);
this.setState = nextState => {
this.state = nextState;
if (isInput) {
input.setState({
title: this.state.title,
});
}
};
}
_appendChilds($modalButtonBox, $conformButton, $cancelButton);
_appendChilds($modal, $modalButtonBox);
this.render = () => {
$target.appendChild($fragment);
};
const onCancel = () => {
$target.removeChild(this.$container);
};
$conformButton.addEventListener(
'click',
async () => await onConform(isInput ? this.state.title : undefined),
);
$cancelButton.addEventListener('click', () => onCancel());
}
또한 onCancel
역시 어차피 그냥 취소는 취소니까, 별도로 매개변수를 받지 않았습니다!
그렇다면, 어떻게 호출되었는지를 볼까요?
import classNames from '../utils/classNames.js';
import {
_removeAllChildNodes,
_createElemWithAttr,
} from '../utils/customDOMMethods.js';
import renderPosts from '../utils/renderPosts.js';
import names from '../utils/classNames.js';
import createPost from '../apis/route/post/createPost.js';
import Modal from './common/Modal.js';
import { push } from '../apis/router.js';
import { ERROR_STATUS } from '../utils/constants.js';
import deletePost from '../apis/route/post/deletePost.js';
import getPostList from '../apis/route/post/getPostList.js';
/*
{
documents: []
}
*/
export default function SideBar({ $target, initialState, onClick }) {
const {
postsItem,
postsBlock,
sideBarItem,
postBlock,
postToggleBtn,
postNext,
postNextNew,
postRemoveBtn,
} = names;
const $sideBar = document.createElement('nav');
$sideBar.className = classNames.sideBarContainer;
const $posts = _createElemWithAttr('section', [sideBarItem, postsBlock]);
this.state = initialState;
this.setState = nextState => {
if (JSON.stringify(this.state) !== JSON.stringify(nextState)) {
_removeAllChildNodes($posts);
this.state = nextState;
const { documents } = this.state;
const $fragment = new DocumentFragment();
renderPosts($fragment, documents);
$posts.appendChild($fragment);
$sideBar.appendChild($posts);
}
this.render();
};
this.render = () => {
$target.appendChild($sideBar);
};
$sideBar.addEventListener('click', e => {
if (!e.target.classList.contains(postsItem)) return;
const postId = e.target.getAttribute(['data-id']);
onClick(postId);
});
$posts.addEventListener('click', e => {
const { target } = e;
if (!target.classList.contains(postToggleBtn)) return;
const closestPostId = target.closest(`.${postBlock}`).dataset.id;
const $nextItem = $posts.querySelector(
`.${postNext}[data-id="${closestPostId}"]`,
);
$nextItem.classList.toggle('invisible');
target.classList.toggle('toggle');
});
$sideBar.addEventListener('click', e => {
const closestPostNextNew = e.target.closest(`.${postNextNew}`);
if (!closestPostNextNew) return;
const $app = document.querySelector('#app');
const closestPostNext = e.target.closest(`.${postNext}`);
const modal = new Modal({
$target: $app,
head: '생성할 페이지의 제목을 입력해주세요!',
isInput: true,
onConform: async title => {
try {
const result = await createPost(this.state.username, {
title,
parent: closestPostNext.dataset.id,
});
push(`/posts/${result.id}`);
} catch (e) {
console.error(e);
alert(ERROR_STATUS, e);
}
},
});
modal.render();
});
$sideBar.addEventListener('click', e => {
if (!e.target.classList.contains(postRemoveBtn)) return;
const $app = document.querySelector('#app');
const closestPostNext = e.target.closest(`.${postsItem}`);
const modal = new Modal({
$target: document.querySelector('#app'),
head: '정말로 삭제하시겠어요?',
isInput: false,
onConform: async () => {
try {
await deletePost(this.state.username, closestPostNext.dataset.id);
const posts = await getPostList(this.state.username);
this.setState({
documents: posts,
});
} catch (e) {
console.error(e);
alert(ERROR_STATUS, e);
} finally {
$app.removeChild(modal.$container);
}
},
});
modal.render();
});
}
저는 렌더링을 할 때, sideBar
에서 기존 데이터 + 삭제된 데이터가 합쳐져서 렌더링되는 현상이 발생했답니다.
여기서 정~말 많은 고민을 했고, 정말 화났었는데, 알고 보니 저는 이걸 간과했어요.
_removeAllChildNodes($posts);
원래는 $sideBar
을 대상으로 썼답니다. 이때, 현재 데이터를 갖고 있는 건 $post
였는데요, 그냥 단순하게 $sideBar
에서 빠지면 사라지는 거 아냐?! 싶었는데, 아닌 걸, 얘가 형체 그대로 살아 있더라구요😂😂😂
createElement
가 꽤나 생명력이 질기군요. 참 무서운 애니까 조심해야겠어요!
또한 DocumentFragment
역시 간과했답니다. 이 친구는 createElement
랑 반대로 넣은 다음에 나 역할 끝났다?!하고 홀라당 가버립니다.
따라서 이를 페이지를 만들 때 썼었는데, 렌더링 하자마자 도망가버리는 이녀석은... 참 도덕 책...📖📖🔥🔥🔥
여튼 이 두 가지를 해결하고 보니, 5시 반이지만, 기분은 굉장히 좋았어요.
몰랐던 걸 알아내는 것만큼이나, 좋은 일은 없으니까요!😄
절대 못잊는다
이제 아가들 옷입히러(?) 가봅시다
@import "../color";
.sidebar-container {
position: fixed;
top: 0;
width: 300px;
height: 100vh;
overflow-y: scroll;
padding: 1rem;
background: $mint100;
.sidebar__item.posts {
width: 100%;
.post {
display: flex;
align-items: center;
width: 100%;
margin: {
right: auto;
bottom: 0.5rem;
}
&:hover {
cursor: pointer;
}
&__link {
text-decoration: none;
color: black;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
&__toggle-btn {
transition: all 0.3s;
&:hover {
color: rgb(255, 145, 0);
}
}
&__remove-btn {
margin-left: auto;
}
&__new-button {
display: flex;
align-items: center;
}
}
.post-next {
padding-left: 1.375rem;
&__new {
display: flex;
align-items: center;
transition: all 0.3s;
margin-bottom: 1rem;
&:hover {
cursor: pointer;
color: rgb(255, 145, 0);
}
}
&__new-icon {
margin-right: 0.25rem;
}
}
}
}
css가 이렇게 재밌는 줄 몰랐어오! 고통 끝에서의 새로운 발견입니다!
그리고, fetch
에서 발전된 request
에서 발전된(...) createPost
와 deletePost
메서드를 만들어주었어요!
import request from '../../request.js';
const createPost = async (username, body) => {
return await request(`/documents`, {
options: { body: JSON.stringify(body), method: 'POST' },
header: {
'x-username': username,
},
});
};
export default createPost;
import request from '../../request.js';
const deletePost = async (username, id) => {
return await request(`/documents/${id}`, {
options: {
method: 'DELETE',
},
header: {
'x-username': username,
},
});
};
export default deletePost;
그럼, 결과를 볼까요?!
잘 작동하네요😄
공부한지 14시간째, 슬슬 졸려 미칠 듯한 타이밍입니다...!
하지만 저는 오늘 밤을 새기로 했어오. (부제: 네 실력에 잠이오냐?!)
40시간, 버틸 수 있겠죠...?!
일해라, 청춘아!!😂😂😂