ํ๋ก ํธ์๋๋ฅผ ๊ณต๋ถํ๋ฉฐ,ํ๋ฉด ํ ์๋ก ๊ฒฐ๊ตญ ๊ธฐ๋ฐ์ธ Javascript๊ฐ ์ค์ํ๋ค๋ ์๊ฐ์ด ๋ค์๋ค.๊ทธ๋์ ํ๋ ์ ์ํฌ๋ฅผ ์ฌ์ฉํ์ง ์๊ณ ๋ ๊ฐ๋ฐํ ์ ์๋ ๋ฅ๋ ฅ์ ์ฌ๋ฆฌ๊ณ ์ถ์ด ์ฐธ์ฌํ๊ฒ ๋์๋ค.
๋ฐฐํฌ ๋งํฌ : HPNY-2023
router.js
const ROUTE_CHANGE_EVENT = 'ROUTE_CHANGE';
export const init = (onRouteChange) => {
window.addEventListener(ROUTE_CHANGE_EVENT, () => {
onRouteChange();
});
};
export const routeChange = (url, params) => {
history.pushState(null, null, url);
window.dispatchEvent(new CustomEvent(ROUTE_CHANGE_EVENT, params));
};
SPA ๋ผ์ฐํ
์ ๊ตฌํํ๊ธฐ ์ํด์ ์ปค์คํ
์ด๋ฒคํธ๋ฅผ ํตํด์ History๋ณ๊ฒฝ์ ๊ฐ์งํ๊ณ ,
ํ์ด์ง ์ด๋์ด ์๋ ์ํ๋ ๋์(onRouteChange)์ ํ ์ ์๋๋ก ํ์๋ค.
App.js
class App {
$target;
constructor({ $target }) {
this.$target = $target;
init(this.route.bind(this)); // history ๋ณ๊ฒฝ ๊ฐ์ง,
window.addEventListener('popstate', this.route.bind(this));
this.route();
}
route() {
const { pathname } = location;
this.$target.innerHTML = `
<header class='header'></header>
<div class='content'></div>
`;
const $content = this.$target.querySelector('.content');
const $header = this.$target.querySelector('.header');
new Header($header);
if (pathname === '/') {
new Home($content, { title: 'Home' });
} else if (pathname.indexOf('post') === 1) {
const [, , postId] = pathname.split('/');
new DetailPage($content, { postId, title: 'Detail' });
} else if (pathname.indexOf('edit') === 1) {
const [, , postId] = pathname.split('/');
new Edit($content, { postId, title: 'Edit' });
} else if (pathname === '/write') {
new WritingPage($content, { title: 'Write' });
}
}
}
init()ํจ์์ parameter๋ก routeํจ์๋ฅผ ์ ๋ฌํด,
history ๋ณ๊ฒฝ ์์ ํ์ด์ง ์ด๋์ด ์๋ route ํจ์๋ฅผ ์คํํ ์ ์๋๋ก ํ์๋ค.
route ํจ์์์๋ path์ ๋ฐ๋ผ ํด๋นํ๋ Component๋ฅผ ํ์ด์ง์ ๋ณด์ฌ์ค ์ ์๊ฒ ํ๋ค.
๋ํ, ๋ค๋ก๊ฐ๊ธฐ๋ฅผ ๋๋ฅธ๊ฒฝ์ฐ์๋ popstate
์ด๋ฒคํธ๋ฅผ ๋ฐ์์ด ๋๋๋ฐ, popstate ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ๋ฉด routeํจ์๋ฅผ ์คํ์์ผ, SPA ๋ค๋ก๊ฐ๊ธฐ ๋์์ ๊ตฌํํ์๋ค.
Component.js
import { routeChange } from '@/router';
class Component {
state;
props;
$target;
constructor($target, props) {
this.$target = $target;
this.props = props;
this.init();
this.render();
}
init() {} // ์ปดํฌ๋ํธ๊ฐ ์์ฑ๋์์๋, ์ฒ์ ํ๋ฒ๋ง ์คํ
setState(nextState) {
this.state = { ...this.state, ...nextState };
this.render();
}
view() { // Component์ dom ๊ตฌ์กฐ
return ``;
}
mount() {} // dom์ด ์ถ๊ฐ๋ ์ดํ์ ์คํ๋์ด์ผ ํ ๋์๋ค
render() {
this.$target.innerHTML = this.view();
this.mount();
}
navigate(url, params) { // SPA ๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํ ํ์ด์ง ์ด๋ ํจ์
routeChange(url, params);
}
querySelectorChild(selector) {
return this.$target.querySelector(selector);
}
}
export default Component;
๊ณตํต์ ์ผ๋ก ์ฌ์ฉํ๋ ์ฝ๋๋ค์ class ๋ฌธ๋ฒ์ผ๋ก ์ถ์ํ ์์ผ, ํ์ํ ๋ถ๋ถ์์๋ ์์์ ๋ฐ์ ๋ณ๊ฒฝํ ์ ์๋๋ก ๊ตฌํํ์๋ค.
- state๊ฐ ๋ณ๊ฒฝ๋๋ฉด render์ ์คํํ๋ค.
- state๋ setState๋ก๋ง ๋ณ๊ฒฝ์ ํด์ผํ๋ค.
์์ ๋ ๊ท์น์ ์ง์ผ ์ฝ๋๋ฅผ ์์ฑํ ์ ์๋๋ก ํ์๋ค.
๋ฐ์ Comment.js ์ฝ๋๋ Component๋ฅผ ์ฌ์ฉํ ์์์ด๋ค.
Comment.js
class Comment extends Component {
view() {
const { content } = this.props.comment;
return `
<div class="comment__content">${content}</div>
<button class="comment__delete-btn"></button>
`;
}
mount() {
this.querySelectorChild(`.comment__delete-btn`).addEventListener(
'click',
this.handleCommentDelete.bind(this),
);
}
async handleCommentDelete() {
const { comment, refetch } = this.props;
await deleteComment(comment.commentId);
}
}
view ํจ์์ Comment Component์ DOM ๊ตฌ์กฐ๋ฅผ ์์ฑํด ๋ฆฌํดํ๊ณ ,
mount ํจ์์์ ๋ฒํผ์ ๋๊ธ์ ์ญ์ ํ๋ ์ด๋ฒคํธ๋ฅผ ์ถ๊ฐํ์๋ค.
๋๊ธ ๋ด์ฉ (content)๋ ์ปดํฌ๋ํธ์ props๋ก ๋ฐ์, class ๋ด๋ถ์์ ์ฌ์ฉ ํ ์ ์๋๋กํ์๋ค.
api.js
export const requestPOST = async (url, body) => {
try {
const response = await fetch(`${BASE_PATH}${url}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
const json = await response.json();
if (response.ok) {
return json;
}
if (json.code === 400) {
throw new Error(json.message);
}
throw new Error('API ํต์ ์คํจ');
} catch (error) {
throw error;
}
};
api์ ๊ฒฝ์ฐ์๋ GET, POST, DELETE, PATCH๋ง๋ค api ํ ํ๋ฆฟ์ ๋ง๋ค์ด, ํด๋น ๋ฉ์๋์ url, body๋ฅผ parameter์ ๋๊ฒจ์ ์ฌ์ฉํ์๋ค.
๋ง์ฝ error๊ฐ ๋ฐ์ํ ๊ฒฝ์ฐ, throw error์ ์ด์ฉํ์ฌ ํด๋น api๋ฅผ ์ฌ์ฉํ๋ ์ฝ๋์์ ์ปค์คํ ํ์ฌ ์๋ฌ ํธ๋ค๋ง์ ํ ์ ์๋๋ก ํ์๋ค.
express๋ฅผ ์ด์ฉํด์ ์๋ฒ๋ฅผ ๊ตฌํํ๊ธฐ๋ ๋ฒ๊ฑฐ๋ก์ ๊ณ ,
vite์ build์๋๊ฐ ๋งค์ฐ ๋นจ๋ผ ์ข๋ค๊ณ ์๊ณ ์์ด์ ๋ฒ๋ค๋ฌ๋ก vite๋ฅผ ์ ํํ์๋ค.
vallina js๋ฅผ ์ด์ฉํ์ฌ React์ฒ๋ผ ์ปดํฌ๋ํธ ๊ธฐ๋ฐ ๊ตฌ์กฐ๋ก ์์ฑํ๋ ค๊ณ ํ์๋ค.
๋น์ทํ๊ฒ ๊ตฌํํ๋ ค๊ณ ํ๋ค๋ณด๋ ์๊ฐํ๋๊ฒ ๋ณด๋ค ์ฝ์ง ์์๊ณ , ๋ ๋๋ง ์ด์ ๋ฑ์ด ์๊ฒผ๋๋ฐ, ๊ฒฐ๊ตญ ์ด ๋ฌธ์ ๋ ๊ฐ์๋์ ์ฐ์ง ์๊ธฐ ๋๋ฌธ์ ์ด์ฉ์ ์์๋๊ฒ ๊ฐ๋ค.
๊ทธ๋์ React์๋ ์กฐ๊ธ ๋ค๋ฅด๊ฒ ๊ธฐ๋ฐ dom์ ์์ฑํ๊ณ , ๊ทธ ์์น์ ์ปดํฌ๋ํธ๋ฅผ ์์ฑํด์ฃผ๋ ๋ฐฉ๋ฒ์ ์ด์ฉํ์๋ค. (์์ฑํ๊ณ ์ถ์ ์์น์ dom์ ๋ฏธ๋ฆฌ ์์ฑํด์ผํ๋ค๋ ๋จ์ ์ด ์์ง๋ง,)
์ฒ์์ function ํจ์๋ฅผ ์ด์ฉํ์ฌ์ ๊ตฌํ์ ํ์๋ค. ๊ทธ๋ฐ๋ฐ setState์ ๊ฐ์ ๋ก์ง๋ค์ด ๊ณ์ํด์ ์ค๋ณต๋๋ ๋ฌธ์ ๊ฐ ์๊ฒผ๋ค.
๊ทธ๋์ ์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด es6 class๋ฌธ๋ฒ์ ์ฌ์ฉํ ์์์ ์ด์ฉํ๊ธฐ๋ก ํ์๋ค.
๊ณตํต์ ์ธ ํจ์๋ค์ ๊ธฐ๋ฐ ์ปดํฌ๋ํธ์ ๊ตฌํํด๋๊ณ , ํ์ํ๋ค๋ฉด ํจ์๋ค์ overrideํ๊ฑฐ๋ ์๋ก ๋ง๋ค์ด์ ์ฌ์ฉํ ์ ์๋๋ก ํ์๋ค.
function (์ ) | class (ํ) |
---|---|
js์ innerHTML์ ๊ฒฝ์ฐ, cross site scripting ๊ณต๊ฒฉ์ ์ทจ์ฝํ๋ค๋ ๋ณด์์์ ๋ฌธ์ ๊ฐ ์์ด, ์ปดํฌ๋ํธ ๊ธฐ๋ฐ ๊ตฌ์กฐ๋ก ๊ตฌํ์ ํ๋ฉด์ ๊ฐ๋ฅํ appendChild๋ฅผ ์ด์ฉํ์ฌ ๊ตฌํ์ ํ๊ณ ์ถ์๋ค.
ํ์ง๋ง ๊ทธ๋ ๊ฒ ๋๋ฉด ๋ฆฌ๋ ๋๋ง์ด ๋์ง ์๊ฑฐ๋, ๋ ๋๋ง ๋ ๋ ๋ง๋ค ๋์ผํ ์ปดํฌ๋ํธ๊ฐ ์ถ๊ฐ๋๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค.
๊ทธ๋์ ์ด์ฉ์ ์์ด innerHTML์ ์ด์ฉํ์ฌ ๊ธฐ๋ฐ ์ปดํฌ๋ํธ๋ฅผ ๊ตฌํํ ์ ๋ฐ์ ์์๋ค.
๊ตฌํํ render() - Component.js
๐