[Project] 모달을 통한 생성/삭제 기능 구현

young_pallete·2021년 9월 2일
0

no#tation

목록 보기
6/11

시작하며 🔥

예... 현재 12시간 프로젝트 중인데, 같은 부분에서 3시간 삽질했네요...!
인내심이 끝까지 달리던 순간, 뭔가 제가 오해가 있을 거란 부분을 생각해서 수정한 결과, 정상 동작을 하게 됐읍니다!!! 👏👏👏

이래서 정말... 언러닝이 중요하다는 것을 다시금 느낍니다.
그렇다면 어떻게 구현했는지 살펴보시죠!

본론 📃

저는 모달을 만들고 싶었어요. 아무래도 생성하거나 삭제할 때 충분한 정보 전달에도 용이하기도 하고, 함부로 api가 사용되지 않게 함에도 제격이니까요.

따라서! 다음과 같은 모달을 만들어주었습니다.

Modal.js

이 모달은 말이죠, 현재 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 역시 어차피 그냥 취소는 취소니까, 별도로 매개변수를 받지 않았습니다!

그렇다면, 어떻게 호출되었는지를 볼까요?

sideBar.js

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시 반이지만, 기분은 굉장히 좋았어요.

몰랐던 걸 알아내는 것만큼이나, 좋은 일은 없으니까요!😄 절대 못잊는다

sidebar.scss

이제 아가들 옷입히러(?) 가봅시다

@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에서 발전된(...) createPostdeletePost 메서드를 만들어주었어요!

createPost.js

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;

deletePost.js

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시간, 버틸 수 있겠죠...?!

일해라, 청춘아!!😂😂😂

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

0개의 댓글