좋은 폴더 구조의 장점
이번 프로젝트를 위한 설계
UI Component
mvc + obsever 패턴
(출처: https://jin-pro.tistory.com/80)
두 패턴을 적용한 이유
실제 적용 예시
observer.js
let currentObserver = null;
export const observe = (fn) => {
currentObserver = fn;
fn();
currentObserver = null;
};
export const observable = (obj) => {
Object.keys(obj).forEach((key) => {
let _value = obj[key];
const observers = new Set();
Object.defineProperty(obj, key, {
get() {
if (currentObserver) observers.add(currentObserver);
return _value;
},
set(value) {
_value = value;
observers.forEach((fn) => fn());
},
});
});
return obj;
};
store.js
import { observable } from "../component/core/observer.js";
export const store = {
state: observable({
movieList: [],
}),
setState(newState) {
for (const [key, value] of Object.entries(newState)) {
if (!this.state[key]) continue;
this.state[key] = value;
}
},
};
Component.js
import { observable, observe } from "./observer.js";
export default class Component {
state;
props;
$el;
constructor($el, props) {
this.$el = $el;
this.props = props;
this.setup();
}
setup() {
this.state = observable(this.initState());
observe(() => {
this.render();
this.setEvent();
this.mounted();
});
}
initState() {
return {};
}
template() {
return "";
}
render() {
this.$el.innerHTML = this.template();
}
setEvent() {}
mounted() {}
addEvent(eventType, selector, callback) {
this.$target.addEventListener(eventType, (event) => {
if (!event.target.closest(selector)) return false;
callback(event);
});
}
}
searchView.js
import Component from "../../core/Component.js";
import { store } from "../../../store/store.js";
import fetchGet from "../../../utils/apis/fetchGet.js";
import { MOVIE_API_KEY } from "../../../../constants/constants.js";
export default class SearchView extends Component {
template() {
return `
<div id="searchTitle" class="searchTitle">영화 검색:</div>
<input type="text" id="searchInput" placeholder="영화 제목을 검색해 보세요"" />
<button id="searchBtn" class="searchBtn">검색</button>
`;
}
async setEvent() {
const { $el } = this;
const search = $el.querySelector("#searchInput");
$el.querySelector("#searchBtn").addEventListener("click", () => {
fetchGet(
`https://api.themoviedb.org/3/search/movie?api_key=${MOVIE_API_KEY}&language=ko-KR&query=${search.value}&page=1&include_adult=false`
).then((res) => {
store.setState({ movieList: res.results });
});
});
}
}
searhView에서 데이터 변경 -> store 변경(Obseverable) -> obsever에 등록되어 있던 함수 실행 -> Component 렌더링 -> view 업데이트
위 과정을 통해서 searchView에서는 원하던 데로 데이터만 업데이트 할 수 있게 되었다!
export default class MovieView extends Component {
template() {
return `
<div id="movieContainer" class="movieContainer"></div>
`;
}
mounted() {
const { $el } = this;
store.state.movieList.forEach(
({ poster_path, original_title, overview, vote_average, id }) => {
const $view = $el.querySelector(`#movieContainer`);
new Card($view, {
src: poster_path,
title: original_title,
content: overview,
rating: vote_average,
id: id,
});
}
);
}
}
render() {
this.$el.innerHTML = this.template();
}
export default class MovieView extends Component {
template() {
return `
<div id="movieContainer" class="movieContainer">
${store.state.movieList
.map(
({ id }) => `
<div class="movieCard-${id}" id="movieCard-${id}"></div>
`
)
.join("")}
</div>
`;
}
mounted() {
const { $el } = this;
store.state.movieList.forEach(
({ poster_path, original_title, overview, vote_average, id }) => {
const $view = $el.querySelector(`#movieCard-${id}`);
new Card($view, {
src: poster_path,
title: original_title,
content: overview,
rating: vote_average,
id: id,
});
}
);
}
}
내가 작성한 README
과거에 본 README 중 잘 썼다고 생각한 걸 참고했지만 쓰다 보니까 이게 가장 좋은 README 방식인가? 라는 생각도 든다. 다른 사람들은 README를 어떻게 쓰는지 조금 더 찾아봐야겠다.