제한사항
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 인스턴스가 계속 생기게 된다!