프론트엔드 객체지향 설계 요점정리
응답 데이터가 바뀌면 화면도 바뀌도록 설계하는 것이 최우선입니다
이벤트는 필요한 경우에 한 번만 생성되도록 설계해야 합니다
(화면을 새로 그릴 때마다 이벤트가 재생성되지 않도록 해야 해요)
부모 클래스(component
)에서는 함수의 실행순서 정리 및 호출만,
함수의 구체적인 내용은 자식 클래스(app
)에서 구현하는 것이 나중에 알아보기 편합니다
(함수와 변수명은 이름만으로도 역할을 유추할 수 있게끔 구체적으로)
서버에서 직접 데이터(텍스트)를 받아와서 그릴 것인지,
true / false값만 반환받고 클라이언트측에서 그릴 것인지는 선택사항입니다
되도록 서버에 가는 부담을 줄이는 편이 나아보이기는 합니다
↓ 예제 및 기록보관용 코드입니다
[Component]
class Component {
target; // 앞으로 넣을 Element
state; // 데이터들
constructor(_target) {
this.target = _target;
this.setup();
this.render();
this.setEvent(); // setEvent는 render가 끝난 후에(버튼이 생겨난 후에) 호출되어야 함
}
// 부모 클래스에서는 함수 호출만
setup() {} // 자식클래스에서 구현
template() {} // 자식클래스에서 구현
render() {
this.target.innerHTML = this.template(); // render 메서드가 호출될 때 template 함수 실행
}
setEvent() {}
addEvent(type, selector, callback) {
// type : click, mouseover, mouseleave...
// selector : 이벤트를 추가할 엘리먼트 선택자
// console.log(selector)
// console.log(document.querySelectorAll(selector))
const children = [...document.querySelectorAll(selector)]
// 논리연산자 응용
// const b = null && 10 && undefined // b는 null... false값을 먼저 찾기 때문
// const c = console.log("hello") || 10 // console.log가 실행되고 c는 10
// const d = 10 || console.log("world") // 이미 true값을 찾았기 때문에 console.log 실행 x
const isTarget = (target) => children.includes(target) || target.closest(selector)
// true or false를 반환하는 함수입니다
// .btn의 부모요소 div 혹은 내부의 a태그를 선택해도 클릭이벤트가 발동되도록 (포괄적으로)
this.target.addEventListener(type, (e)=>{
if (!isTarget(e.target)) return false
callback(e)
})
}
setState(newState) {
if (this.state === newState) return; // 데이터가 바뀌면 화면도 바뀌어야 합니다
this.state = { ...this.state, ...newState };
this.render(); // 클릭 이벤트로 setState 실행 > render 함수 호출 > 화면이 바뀜
}
}
export default Component;
[app]
import Component from "/js/core/Component.js";
class App extends Component {
setup() {
this.state = {
list: [
{
id: 1,
userid: "web7722",
content: "hello1",
register: "2023-01-09",
updated: false,
},
{
id: 2,
userid: "web7722",
content: "hello2",
register: "2023-01-09",
updated: false,
},
{
id: 3,
userid: "web7722",
content: "hello3",
register: "2023-01-09",
updated: false,
},
],
user: {
userid: "web7722",
username: "ingoo",
},
};
}
content(content) {
return `
<span class="comment-update-btn">${content}</span>
<span class="comment-delete-btn">❌</span>
`;
}
update(content) {
return `
<span>
<input type="text" class="comment-update-input" value="${content}" data-value="" />
</span>
<span class="comment-delete-btn">❌</span>
`;
}
template() {
const { list } = this.state;
return `
<ul class='comment'>
<li class='comment-form'>
<form id='commentFrm'>
<h4>
댓글쓰기
<span></span>
</h4>
<span class='ps_box'>
<input type='text' class='int' name='content' placeholder='댓글입력점.'>
</span>
<button type='submit' class='btn'>등록</button>
</form>
</li>
<li id='comment-list'>
${list
.map((comment) => {
return `<ul class="comment-row" data-index="${
comment.id
}">
<li class="comment-id">${
comment.userid
}</li>
<li class="comment-content">
${
comment.updated
? this.update(comment.content)
: this.content(comment.content)
}
</li>
<li class="comment-date">${
comment.register
}</li>
</ul>`;
})
.join("")}
</li>
</ul>
`;
}
setEvent() {
// ↓ 지양할 코드...render가 실행될 때마다 클릭이벤트가 재생성됩니다
// this.target.querySelector("#btn").addEventListener("click",()=>{
// console.log("click test");
// const {list} = this.state
// this.setState({ list: [...list, { id: 4, userid: "web7722", content: "hello4", register: "2023-01-09" }] })
// })
// ver2 원하지 않는 요소를 클릭해도 이벤트가 발동됩니다
// this.target.addEventListener('click',(e)=>{
// const list = [...this.state.list]
// list.push({ id: 4, userid: "web7722", content: "hello4", register: "2023-01-09" })
// if (e.target.classList.contains('btn')) {
// this.setState({list})
// }
// })
this.addEvent("submit", "#commentFrm", (e) => {
e.preventDefault();
const { list } = this.state;
this.setState({
list: [
...list,
{
id: list.length,
userid: "web7722",
content: e.target.content.value,
register: "2023-01-09",
updated: false,
},
],
});
});
this.addEvent("click", ".comment-update-btn", (e) => {
const ul = e.target.closest(".comment-row");
const { index } = ul.dataset;
const list = [...this.state.list];
const newList = list.map((v) => {
if (v.id === parseInt(index)) v.updated = true;
return v;
});
console.log(newList);
this.setState({ list: newList });
});
this.addEvent("click", ".comment-delete-btn", (e) => {
const ul = e.target.closest(".comment-row");
const { index } = ul.dataset;
// 요청때리고
// 응답결과
const list = [...this.state.list].filter((v) => v.id !== parseInt(index));
this.setState({ list });
});
this.addEvent("keypress", ".comment-update-input", (e) => {
if (e.keyCode !== 13) return;
const ul = e.target.closest(".comment-row");
const index = parseInt(ul.dataset.index);
// 요청
// 응답
const list = this.state.list.map((v) => {
if (v.id === index) {
v.content = e.target.value;
v.updated = false;
}
return v;
});
this.setState({ list });
});
// document.querySelector("click", () => {})
}
}
export default App;