๐Ÿ”จ ํ”„๋กœ์ ํŠธ ์ผ์ง€

์ •๋ณด ์ „๋‹ฌ๋ณด๋‹จ ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋ฉฐ ๊ฒช์€ ์ ๋“ค, ๋Š๋‚€ ์ ๋“ค์„ ๊ธฐ๋กํ•œ ์ผ์ง€

Project: Get Shit Done

๋‚˜๋งŒ์˜ ToDo๋ฆฌ์ŠคํŠธ๋ฅผ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด ์‹œ์ž‘ํ•œ ํ”„๋กœ์ ํŠธ
Get Shit Done์€ ๊ทธ๋ƒฅ ๋‹ฅ์น˜๊ณ  ํ•ด๋ผ๋Š” ์˜๋ฏธ๋กœ ํ‰์ƒ์‹œ ์Šค์Šค๋กœ ๋‹ค์งํ•˜๋Š” ๋ฌธ์žฅ์ธ๋ฐ, ํˆฌ๋‘๋ฆฌ์ŠคํŠธ์™€ ์–ด์šธ๋ฆฐ๋‹ค ์ƒ๊ฐํ•˜์—ฌ ํ”„๋กœ์ ํŠธ๋ช…์œผ๋กœ ์ •ํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค.
์ด๋ฒˆ ํ”„๋กœ์ ํŠธ์˜ ๊ฐ€์žฅ ํฐ ๋ชฉ์ ๊ณผ ์˜์˜๋Š” ์›นํŒฉ ๋ฐ ๋ฐ”๋ฒจ์„ ํ†ตํ•ด ๊ฐ ๋ชจ๋“ˆ์„ ์›น ์ปดํฌ๋„ŒํŠธ๋กœ ๊ตฌ์„ฑํ•˜์—ฌ ๋™์ž‘ํ•˜๊ฒŒ ํ•˜๋Š” ๊ฒƒ์ด๋‹ค!!!

๐Ÿคช ์ปดํฌ๋„ŒํŠธ ๋กœ์ง

๋จผ์ € ๋“ค์–ด๊ฐ€๊ธฐ์— ์•ž์„œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋‚˜๋ˆˆ ๋ถ€๋ถ„์„ ๋‹ค์‹œ ๋ณด์ž๋ฉด, ํฌ๊ฒŒ ์•„๋ž˜์™€ ๊ฐ™์ด ๋‚˜๋ˆ„์—ˆ๋‹ค.

  • todoApp.js: ์ „์ฒด ์•ฑ์„ ๋‚˜ํƒ€๋‚ธ๋‹ค.
  • todoContainer.js: ์•ฑ ๋‚ด์— ๋ฉ”๋ชจ์žฅ์„ ๋‚˜ํƒ€๋‚ธ๋‹ค.(์—ฌ๋Ÿฌ๊ฐœ์˜ ๋ฉ”๋ชจ์žฅ์„ ์ƒ์„ฑํ•˜๊ฒŒ ํ•  ์˜ˆ์ •)
  • todoToolbar.js: ๋ฉ”๋ชจ์— ๋Œ€ํ•œ ์กฐ์ž‘์„ ํ•˜๋Š” ํˆด๋ฐ” ๋ถ€๋ถ„
  • todoNote.js: ํˆด๋ฐ” ์กฐ์ž‘์— ๋”ฐ๋ผ ๋ฉ”๋ชจ๋ฅผ ์ž…๋ ฅํ•  ์ˆ˜ ์žˆ๋Š” input ๋ถ€๋ถ„
  • todoList.js: ๋ฉ”๋ชจ ์ „์ฒด๋ฅผ ๊ฐ์‹ธ๊ณ  ์žˆ๋Š” ๋ถ€๋ถ„
  • todoItem.js: ๋ฉ”๋ชจ ์ปดํฌ๋„ŒํŠธ
๐Ÿงญ todoNote

todoNote๋Š” item(memo)์„ ์ถ”๊ฐ€ํ•˜๊ธฐ ์œ„ํ•œ ์ปดํฌ๋„ŒํŠธ๋กœ memo์˜ ๋‚ด์šฉ์„ ์ž…๋ ฅํ•˜๋Š” ๋ถ€๋ถ„์ด๋‹ค.

connectedCallback() {
    // 1. Add ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ์ƒˆ๋กœ์šด ๋ฉ”๋ชจ ์ƒ์„ฑ
    const $noteAddButton = this.shadowRoot.querySelector('.note-add-button');
    const $noteInput = this.shadowRoot.querySelector('.note-input');
    $noteAddButton.addEventListener('click', (event) => {
      // 1-1. input์ด ์—†์„ ๋•Œ๋Š” ๋ฉ”๋ชจ ์ƒ์„ฑ ๋ง‰๊ธฐ
      if (!$noteInput.value) return;

      const $todoItem = document.createElement('todo-item');
      $todoItem.dataset.containerTitle = this.dataset.containerTitle;
      $todoItem.dataset.itemTitle = $noteInput.value;
      this.nextElementSibling.shadowRoot.append($todoItem);

      // 1-2. ๋ฉ”๋ชจ ๊ฐฏ์ˆ˜ ๋”ํ•˜๊ธฐ
      const $noteCount =
        this.previousElementSibling.shadowRoot.querySelector('.count-item');
      $noteCount.textContent = +$noteCount.textContent + 1;

      // 1-3. Add ๋ฒ„ํŠผ ๋น„ํ™œ์„ฑํ™”
      $noteAddButton.style.opacity = '50%';

      // 1-4. Add ์‚ฌ์šฉ์ž ํ™œ๋™ ๊ธฐ๋ก
      const $record = document.createElement('div');
      $record.className = 'record';
      $record.innerHTML = `<span class="record-important">@Jayden</span> added <span class="record-important">${$noteInput.value}</span> to <b>${this.dataset.containerTitle}</b>`;

      const now = Date.now();
      $record.dataset.timeMakeNote = now;
      activityLog.push($record);

      // 1-5. Add ๋ฒ„ํŠผ ํด๋ฆญ ํ›„ ์ž…๋ ฅ์นธ ์ดˆ๊ธฐํ™”
      $noteInput.value = '';
    });
      // 2. Cancel ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ todoNote ๋‹ซ๊ธฐ
      const $noteCancelButton = this.shadowRoot.querySelector(
        '.note-cancel-button'
      );
      $noteCancelButton.addEventListener('click', (event) => {
        this.style.display = 'none';

        // 2-1. ๋ฉ”๋ชจ ๋‹ซ์€ ํ›„ ์ž…๋ ฅ์นธ ์ดˆ๊ธฐํ™”
        $noteInput.value = '';
      });

      // 3. ๋ฉ”๋ชจ ์ž…๋ ฅ ์‹œ Add ๋ฒ„ํŠผ ํ™œ์„ฑํ™”
      $noteInput.addEventListener('input', (event) => {
        $noteAddButton.style.opacity =
          event.target.value.length === 0 ? '50%' : '100%';
      });
    }

todoNote

  • 1-1. input์— ๊ฐ’์ด ์—†๋Š” ๊ฒฝ์šฐ๋Š” ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๋„๋ก early return์„ ํ™œ์šฉํ–ˆ๋‹ค.

    • ์ด๋Ÿฐ ํŒจํ„ด์€ ์›์น˜ ์•Š์€ ๊ฒฝ์šฐ์—, ์•„์˜ˆ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•˜์ง€ ์•Š๊ธฐ๋•Œ๋ฌธ์— ๋ฉ”๋ชจ๋ฆฌ์ ์ธ ๋ฉด์œผ๋กœ๋„ ๊ต‰์žฅํžˆ ์œ ์šฉํ•˜๋‹ˆ ์ž์ฃผ ์‚ฌ์šฉํ•˜๋„๋ก ํ•˜์ž!
  • 1-2. ์ด์ „ ์žฅ์˜ todoToolbar์— ์žˆ๋Š” memo์˜ ๊ฐฏ์ˆ˜๋ฅผ ๋”ํ•ด์ฃผ๋Š” ๊ธฐ๋Šฅ์ด๋‹ค. ์ด ๋•Œ๋Š” ๋‹คํ–‰ํžˆ(?) toolbar์™€ note ์ปดํฌ๋„ŒํŠธ๊ฐ€ ํ˜•์ œ๊ด€๊ณ„๋ผ ์ ‘๊ทผํ•˜๊ธฐ ํŽธํ–ˆ์ง€๋งŒ, ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋„ˆ๋ฌด ๋ณต์žกํ•˜๊ฒŒ ๋‚˜๋ˆ„๋ฉด ๋” ์ ‘๊ทผํ•˜๊ธฐ ์–ด๋ ค์› ์„ ๊ฒƒ ๊ฐ™๋‹ค.

  • 1-4. ์ด ๋ถ€๋ถ„์€ ์ตœ๊ทผ์— ์ƒˆ๋กœ ์ถ”๊ฐ€ํ•œ ๋กœ์ง์œผ๋กœ, ๋ฉ”๋ชจ์— ๋Œ€ํ•œ ์ถ”๊ฐ€/์ˆ˜์ •/์‚ญ์ œ์— ๋Œ€ํ•œ ๊ธฐ๋ก์„ ๋ณด์—ฌ์ฃผ๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•จ์ด๋‹ค.

  • 1-5. ์ด๋Ÿฐ ๊ฒŒ ๋†“์น˜๊ธฐ ์‰ฌ์šด ๊ฒƒ ๊ฐ™๋‹ค. add ๋ฒ„ํŠผ์œผ๋กœ ๋ฉ”๋ชจ๋ฅผ ์ž‘์„ฑํ•˜๊ณ  ๋‹ค์‹œ input์— ๊ฐ’์„ ๋นˆ ๊ฐ’์œผ๋กœ ์ดˆ๊ธฐํ™”ํ•ด์ค€๋‹ค. ๋ญ”๊ฐ€ ์ด๋Ÿฐ ๋ถ€๋ถ„์ด ์ปดํ“จํ„ฐ์ด๊ธฐ์— ํ•˜๋‚˜ํ•˜๋‚˜ ๋ช…๋ นํ•ด์ค˜์•ผํ•œ๋‹ค๊ณ  ๋Š๋ผ๋Š” ๋ถ€๋ถ„์ด๋‹ค.ใ…‹ใ…‹

  • 3 . ๋ฉ”๋ชจ input์— ๊ฐ’์ด ์ƒ๊ธฐ๋ฉด ์‹œ๊ฐ์ ์œผ๋กœ ๋šœ๋ ทํ•˜๊ฒŒ ํ•˜๊ณ  ๋‹ค์‹œ input๊ฐ’์˜ ๊ธธ์ด๊ฐ€ 0์ด๋˜๋ฉด ๋ฐ˜ํˆฌ๋ช…ํ•˜๊ฒŒ ๋งŒ๋“ ๋‹ค.

๐Ÿงญ todoItem

์ถ”๊ฐ€ํ•  ๋กœ์ง์ด ์ œ์ผ ๋งŽ์•˜๋˜, ๋ฉ”๋ชจ ๊ทธ ์ž์ฒด ์ปดํฌ๋„ŒํŠธ์ธ todoItem!!! ๊ฑฐ๋‘์ ˆ๋ฏธํ•˜๊ณ  ๋ฐ”๋กœ ์ฝ”๋“œ๋กœ!!!

connectedCallback() {
    // 1. item์ด ์ƒ์„ฑ๋˜๊ณ  DOM์— ์ถ”๊ฐ€๋  ๋•Œ ๊ทธ ์•ˆ์— ํ…์ŠคํŠธ๋ฅผ ๋ฐ”๊ฟ”์ฃผ๊ธฐ
    this.shadowRoot.querySelector('.item-content').textContent =
      this.dataset.itemTitle;
    // ์ถ”๊ฐ€) ๋ชจ๋‹ฌ ์ฐฝ input value์—๋„ ๊ฐ™์€ ๊ฐ’ ํ• ๋‹น
    this.shadowRoot.querySelector('.modal-input').value =
      this.dataset.itemTitle;

    // 2. item X button ํด๋ฆญ ์‹œ item ์‚ญ์ œ ๋ฐ count ๋นผ๊ธฐ
    const $itemDeleteButton = this.shadowRoot.querySelector(
      '.item-delete-button'
    );
    $itemDeleteButton.addEventListener('click', (event) => {
      // 2-0. ์ปจํŽŒ ๋ฉ”์‹œ์ง€ false์ธ ๊ฒฝ์šฐ early return
      if (!confirm('์„ ํƒํ•˜์‹  ์นด๋“œ๋ฅผ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?')) return;

      // 2-1. item ์‚ญ์ œ
      this.remove();

      // 2-2. count ๋นผ๊ธฐ
      // โœ…๋‹ค์‹œ: shadow dom ๋ฐ”๊นฅ์œผ๋กœ ๊ฐ€๋Š” ๋‹ค๋ฅธ ๋ฃจํŠธ๋Š” ์—†๋Š”์ง€ ํ™•์ธ
      const containerTitle = this.dataset.containerTitle;
      const $noteCount = document
        .querySelector('todo-app')
        .shadowRoot.querySelector(
          `[
            data-container-title=${containerTitle}
          ]`
        )
        .shadowRoot.querySelector('todo-toolbar')
        .shadowRoot.querySelector('.count-item');
      $noteCount.textContent = +$noteCount.textContent - 1;

      // 2-3. ํ™œ๋™ ๊ธฐ๋ก์— ์‚ญ์ œ ํ™œ๋™ ์ถ”๊ฐ€
      const $record = document.createElement('div');
      $record.className = 'record';
      $record.innerHTML = `<span class="record-important">@Jayden</span> deleted <span class="record-important">${this.dataset.itemTitle}</span> from <b>${this.dataset.containerTitle}</b>`;

      const now = Date.now();
      $record.dataset.timeMakeNote = now;
      activityLog.push($record);
    });

    // 3. ์•„์ดํ…œ ๋”๋ธ” ํด๋ฆญ ์‹œ ์ˆ˜์ • ๋ชจ๋‹ฌ ์ƒ์„ฑ
    this.addEventListener('dblclick', (event) => {
      this.shadowRoot.querySelector('.item-modal-outer').style.display =
        'block';
      this.shadowRoot.querySelector('.item-modal-inner').style.display = 'flex';
    });

    // 4. ๋ชจ๋‹ฌ ์ฐฝ X ํด๋ฆญ ์‹œ ๋ชจ๋‹ฌ ๋‹ซ๊ธฐ
    const $modalCloseButton = this.shadowRoot.querySelector(
      '.modal-close-button'
    );
    const $modalInner = this.shadowRoot.querySelector('.item-modal-inner');
    const $modalOuter = this.shadowRoot.querySelector('.item-modal-outer');

    $modalCloseButton.addEventListener('click', (event) => {
      $modalInner.style.display = 'none';
      $modalOuter.style.display = 'none';
      $modalInner.querySelector('.modal-input').value = this.dataset.itemTitle;
    });

    // 5. ๋ชจ๋‹ฌ ์ฐฝ save button ํด๋ฆญ ์‹œ ๋‚ด์šฉ ๋ณ€๊ฒฝ
    const $modalSaveButton =
      this.shadowRoot.querySelector('.modal-save-button');
    const $modalInput = this.shadowRoot.querySelector('.modal-input');
    const $itemContent = this.shadowRoot.querySelector('.item-content');
    $modalSaveButton.addEventListener('click', (event) => {
      $modalInner.style.display = 'none';
      $modalOuter.style.display = 'none';

      // 5-1. ํ™œ๋™ ๊ธฐ๋ก์— ์ˆ˜์ • ํ™œ๋™ ์ถ”๊ฐ€
      const $record = document.createElement('div');
      $record.className = 'record';
      $record.innerHTML = `<span class="record-important">@Jayden</span> changed <b>${$itemContent.textContent}</b> to <span class="record-important">${$modalInput.value}</span> in <b>${this.dataset.containerTitle}</b>`;

      const now = Date.now();
      $record.dataset.timeMakeNote = now;
      activityLog.push($record);

      $itemContent.textContent = $modalInput.value;
    });
}
  • 2-0. ์ด ๋ถ€๋ถ„๋„ ์•„์ฃผ ์œ ์šฉํ•˜๊ฒŒ ์‚ฌ์šฉํ•œ early return ํŒจํ„ด!
  • 2-2. ๋‚˜๋ฅผ ์ œ์ผ ๊ณ ์ƒ์‹œ์ผฐ๋˜ ๋…€์„โ€ฆ ์ปดํฌ๋„ŒํŠธ ๋ณ„๋กœ shadow dom์ด ํ˜•์„ฑ๋˜๋‹ค๋ณด๋‹ˆ, ์ปดํฌ๋„ŒํŠธ์—์„œ ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์„ ํƒํ•˜๋Š” ๊ฒƒ์ด ๋„ˆ๋ฌด ๊นŒ๋‹ค๋กœ์› ๋‹ค. ํŠนํžˆ๋‚˜, ์ง€๊ธˆ๊ณผ ๊ฐ™์ด ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์„œ๋กœ ๋ถ€๋ชจ, ์ž์‹ ๊ด€๊ณ„์ผ ๋•Œ ์ž์‹ ์ปดํฌ๋„ŒํŠธ์˜ root ๋…ธ๋“œ๊ฐ€ ๋ณธ์ธ์ด๋‹ค๋ณด๋‹ˆ ๊ทธ ์œ„๋กœ ์ ‘๊ทผํ•˜๋Š” ๊ฒŒ ๋ถˆ๊ฐ€๋Šฅํ–ˆ๋‹ค.(๊ฐ€๋Šฅํ•œ์ง€ ์ •๋ง ์—ด์‹ฌํžˆ ๋’ค์กŒ์ง€๋งŒโ€ฆ shadow dom์˜ ๊ฐœ๋…์„ ์ƒ๊ฐํ•ด๋ณด๋ฉด ์•ˆ๋˜๋Š” ๊ฒŒ ๋งž๋‹คโ€ฆ) ๊ทธ๋ž˜์„œ ๊ฒฐ๊ตญ dataset์„ ํ†ตํ•ด ์œ„์—์„œ๋ถ€ํ„ฐ ํ•˜๋‚˜ํ•˜๋‚˜ ์ ‘๊ทผํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์„ ํƒํ–ˆ๋‹ค. ์ถ”ํ›„์— ์ด ๋ถ€๋ถ„์„ ๋‹ค๋ฅด๊ฒŒ ํ•ด๊ฒฐํ•  ์ˆ˜๋Š” ์—†๋Š”์ง€ ๊ผญ ์•Œ๊ณ ์‹ถ๋‹ค.
  • 3 . ์ด์ œ ๋ชจ๋‹ฌ ์ฐฝ์„ ๋งŒ๋“œ๋Š” ๋ถ€๋ถ„์€ ์‰ฝ๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ๋‹ค. ์—ญ์‹œ ๊ณ„์† ํ•ด๋ด์•ผํ•œ๋‹ค.

์ฝ”๋ฉ˜ํŠธ๋ฅผ ๊ตณ์ด ๋‚จ๊ธฐ์ง€ ์•Š์€ ๋ถ€๋ถ„๋“ค์€ ์•ž์„œ ์„ค๋ช…ํ•œ ๋ถ€๋ถ„๋“ค์„ ํ†ตํ•ด ์ดํ•ดํ•  ์ˆ˜ ์žˆ๊ฑฐ๋‚˜ ํฌ๊ฒŒ ์–ด๋ ต์ง€ ์•Š์•˜๋˜ ๋ถ€๋ถ„์ด๋ผ ์ƒ๋žตํ•˜์˜€๋‹ค.(๋ฌผ๋ก  ์ด ์ฝ”๋“œ๋“ค์กฐ์ฐจ๋„ ์ง€์ €๋ถ„ํ•˜์ง€๋งŒโ€ฆ) ์ผ๋‹จ ๊ธฐ๋Šฅ๊ตฌํ˜„์— ์ดˆ์ ์„ ๋งž์ถฐ ํ”„๋กœ์ ํŠธ๋ฅผ ๋ช‡๋ฒˆ ์ง„ํ–‰ํ•˜๋‹ค๋ณด๋‹ˆ, ์ ์  ์ฝ”๋“œ ์ž์ฒด์— ์‹ ๊ฒฝ์„ ์“ฐ๊ณ  ์‹ถ์–ด์ง€๊ณ  ๋”๋” ์•Œ์•„๊ฐ€๊ณ  ์‹ถ์€ ๊ฒŒ ๋งŽ์•„์ง„๋‹ค. ์‚ฌ๋žŒ ์š•์‹ฌ์ด ์ฐธ ๊ทธ๋Ÿฐ ๊ฒƒ ๊ฐ™๋‹ค. ์ฒœ์ฒœํžˆ ๋งˆ์Œ ์žก๊ณ  ๋‚ด๊ฐ€ ํ•  ์ˆ˜ ์žˆ๋Š” ์„ ์—์„œ ๋” ๋‚˜์•„๊ฐ€๋ฉด ๋œ๋‹ค. ์ถ”๊ฐ€๋กœ ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋ž๊ณผ ๋‹คํฌ๋ชจ๋“œ๋ฅผ ๊ตฌํ˜„ํ•ด๋ณผ ์˜ˆ์ •์ด๋‹ค..!