연습장

[Todo list] 02. 환경 세팅 / 리스트 확인 본문

클론코딩해보기/TodoList

[Todo list] 02. 환경 세팅 / 리스트 확인

js0616 2024. 8. 17. 04:38

제한사항

1. 순수 자바스크립트로만 구현

2. SPA 로 구현하기

3. SCSS 전처리기 사용

4. Babel 과 Webpack 으로 환경 세팅

5. Prettier & ESLint 적용


 

다음과 같이 webpack , babel , scss, prettier , eslint 세팅

 

https://js0616.tistory.com/322

 

[최종] 바닐라 javaScript 세팅

webpack (babel + sass + img + html)  + eslint + preitter  // package json 생성npm init -y // webpack 설치npm install --save-dev webpack webpack-cli// babel 관련 설치npm install --save-dev @babel/core @babel/cli @babel/preset-env @babel/plugin-tra

js0616.tistory.com

 

package.json

{
  "name": "fesetting",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "webpack -w",
    "lint": "eslint src --fix"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/cli": "^7.24.8",
    "@babel/core": "^7.25.2",
    "@babel/eslint-parser": "^7.25.1",
    "@babel/plugin-transform-runtime": "^7.24.7",
    "@babel/preset-env": "^7.25.3",
    "babel-loader": "^9.1.3",
    "css-loader": "^7.1.2",
    "eslint": "^8.2.0",
    "eslint-config-airbnb": "^19.0.4",
    "eslint-config-prettier": "^9.1.0",
    "eslint-plugin-import": "^2.25.3",
    "eslint-plugin-jsx-a11y": "^6.5.1",
    "eslint-plugin-prettier": "^5.2.1",
    "eslint-plugin-react": "^7.28.0",
    "eslint-plugin-react-hooks": "^4.3.0",
    "file-loader": "^6.2.0",
    "html-webpack-plugin": "^5.6.0",
    "mini-css-extract-plugin": "^2.9.0",
    "prettier": "^3.3.3",
    "sass": "^1.77.8",
    "sass-loader": "^16.0.0",
    "style-loader": "^4.0.0",
    "webpack": "^5.93.0",
    "webpack-cli": "^5.1.4"
  },
  "dependencies": {
    "@babel/runtime-corejs3": "^7.25.0",
    "core-js": "^3.38.0"
  }
}

 


바닐라 js 로 '컴포넌트 기반' SPA 제작

 

https://junilhwang.github.io/TIL/Javascript/Design/Vanilla-JS-Component/#_2-state-setstate-render

 

Vanilla Javascript로 웹 컴포넌트 만들기 | 개발자 황준일

Vanilla Javascript로 웹 컴포넌트 만들기 9월에 넥스트 스텝open in new window에서 진행하는 블랙커피 스터디open in new window에 참여했다. 이 포스트는 스터디 기간동안 계속 고민하며 만들었던 컴포넌트

junilhwang.github.io

 

해당 글을 읽고 정리하였음..  마침, todo-list 를 예제로 하셨다.

 

https://js0616.tistory.com/308

https://js0616.tistory.com/313

 

해당 내용을 요약해보면 ... 

 

1. core 가 되는 추상 클래스 컴포넌트 정의

2. todo-list 에 필요한 컴포넌트 클래스 정의

3. 인스턴스를 생성하여 구현

 

// ./src/core/Component.js
export default class Component {
  $target; // 부모, 컴포넌트가 위치할 DOM 요소
  props; // 전달하는 요소
  state; // 상태
  constructor($target, props) {
    this.$target = $target;
    this.props = props;
    this.setup();
    this.render();
    this.setEvent();
  }
  setup() {} // 초기 상태를 설정
  mounted() {}
  template() {
    return '';
  } // HTML 작성
  render() {
    // 렌더링
    this.$target.innerHTML = this.template();
    this.mounted(); //  render 후에 mounted가 실행 된다.
  }
  setEvent() {} // 이벤트 작성
  setState(newState) {
    // 상태 업데이트

    this.state = { ...this.state, ...newState };
    this.render();
  }

  addEvent(eventType, selector, callback) {
    // const children = [...this.$target.querySelectorAll(selector)];
    this.$target.addEventListener(eventType, event => {
      if (!event.target.closest(selector)) return false;
      callback(event);
    });
  }
}

 


현재 폴더 구조

 

dist : webpack 사용하여 build 시 생성되는 파일 폴더

src : 소스코드

  img : 이미지 파일

  js : main.js (entry) , App.js , core , components 등 js 파일

  sass : scss 파일 

  index.html : html 파일 


필수구현

입력폼 구현

입력에는 제목과 내용을 입력할 수 있다.
둘중 하나라도 값이 없는경우 사용자에게 알려줘야 한다.
alert 또는 입력창 하단에 문구 노출 방식
첫 페이지가 렌더링 되었을때 제목에 포커스가 맞춰져야 한다.
제출은 키보드의 Enter 를 누르거나 입력 버튼을 누르면 제출이 되어야 한다.
제출 후에는 입력값들이 초기화 되어야 한다.
제출 후 할일 목록에 티켓이 생성되고 티켓에는 제목, 내용, 제출 시간이 표시되어야 한다.


 

컴포넌트가 들어갈 div를 html 에 만들어준다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app"></div>
    <!-- build 전에 확인 -->
    <script src="./src/main.js" type="module"></script>
</body>
</html>

 

entry point 로 main.js 

// src/main.js
import App from './App.js';

new App(document.querySelector('#app'));

 

App.js

// src/app.js
// state , method 관리

import Component from './core/Component.js';
import Item from './components/Item.js';

export default class App extends Component {
  // 초기 state
  setup() {
    this.state = {
      items: [
        {
          seq: 1,
          title: '선배!!',
          contents: '마라탕 사주세요',
          date: '2024.08.17. 02:43',
        },
        {
          seq: 2,
          title: '그럼 혹시..',
          contents: '탕후루도 같이?',
          date: '2024.08.17. 02:45',
        },
      ],
    };
  }

  // 자식 컴포넌트가 위치할 dom 요소
  template() {
    return `
      <div data-component="items"></div>
    `;
  }

  // mounted에서 자식 컴포넌트를 마운트
  mounted() {
    const $items = this.$target.querySelector('[data-component="items"]');

    // new 컴포넌트명('DOM'위치 , 전달할 props)
    new Item($items, {
      item: this.state.items[0],
    });
  }
}

 

state 에 예시 데이터를 넣고

template 에 자식 컴포넌트가 위치할 div 를 만들어주고 

mount 에서 해당 dom 에 인스턴스를 생성하여 연결, 이때 props 로 state 를 넘겨줄 수 있다. 

 

 

Item.js

// src/components/Item.js
import Component from '../core/Component.js';

export default class Item extends Component {
  template() {
    console.log('props.item: ', this.props.item);
    return `
      <div>제목 : 1번 할일 </div>
      <div>내용 : 1번 내용 </div>
      <div>작성시간 : 02:36 (2024-08-17) </div>
    `;
  }
}

 

다음과 같이 Item 컴포넌트의 콘솔에 데이터가 온 것을 확인

 

 

기존의 더미 데이터를 지우고 받아온 props 데이터를 넣어준다. 

 

// src/components/Item.js
import Component from '../core/Component.js';

export default class Item extends Component {
  template() {
    const { item } = this.props;
    return `
      <div>제목 : ${item.title} </div>
      <div>내용 : ${item.contents} </div>
      <div>작성시간 : ${item.date} </div>
    `;
  }
}

 


ItemList 를 추가하여 다음과 같이 만들었다.

App.js -> ItemList -> Item1 , Item2 ... 가 되도록 함

// src/app.js
// state , method 관리

import Component from './core/Component.js';
import ItemList from './components/ItemList.js';

export default class App extends Component {
  // 초기 state
  setup() {
    this.state = {
      items: [
        {
          seq: 1,
          title: '선배!!',
          contents: '마라탕 사주세요',
          date: '2024.08.17. 02:43',
        },
        {
          seq: 2,
          title: '그럼 혹시..',
          contents: '탕후루도 같이?',
          date: '2024.08.17. 02:45',
        },
      ],
    };
  }

  // 자식 컴포넌트가 위치할 dom 요소
  template() {
    return `
    <div data-component="itemList"> </div>
    `;
  }

  // mounted에서 자식 컴포넌트를 마운트
  mounted() {
    const $itemList = this.$target.querySelector('[data-component="itemList"]');

    // new 컴포넌트명('DOM'위치 , 전달할 props)
    new ItemList($itemList, {
      items: this.state.items,
    });
  }
}
import Component from '../core/Component.js';
import Item from './Item.js';

export default class ItemList extends Component {
  template() {
    // console.log('props:', this.props.items);
    let ItemDiv = this.props.items.map(item => {
      return `<div data-ItemList${item.seq} ></div>`;
    });
    return `
      ${ItemDiv.join('')}
    `;
  }
  mounted() {
    this.props.items.map(item => {
      new Item(this.$target.querySelector(`[data-ItemList${item.seq}]`), {
        item: item,
      });
    });
  }
}
// src/components/ItemList.js
import Component from '../core/Component.js';
import Item from './Item.js';

export default class ItemList extends Component {
  template() {
    // console.log('props:', this.props.items);
    let ItemDiv = this.props.items.map(item => {
      return `<div data-ItemList${item.seq} ></div>`;
    });
    return `
      ${ItemDiv.join('')}
    `;
  }
  mounted() {
    this.props.items.map(item => {
      new Item(this.$target.querySelector(`[data-ItemList${item.seq}]`), {
        item: item,
      });
    });
  }
}
// src/components/Item.js
import Component from '../core/Component.js';

export default class Item extends Component {
  template() {
    const { item } = this.props;
    console.log(item.seq, item);
    return `
      <div>제목 : ${item.title} </div>
      <div>내용 : ${item.contents} </div>
      <div>작성시간 : ${item.date} </div>
    `;
  }
}

 

 

state.items 가 늘어나면 Item 인스턴스가 계속 생기게 된다!


 

'클론코딩해보기 > TodoList' 카테고리의 다른 글

[Todo list] 06. 삭제, 정렬  (0) 2024.08.19
[Todo list] 05. 필터 기능  (0) 2024.08.19
[Todo list] 04. 수정 기능  (0) 2024.08.18
[Todo list] 03. 리스트 추가하기  (0) 2024.08.18
[Todo list] 01. 요구사항 확인  (0) 2024.08.07