개요
이전의 포스팅에서 컴포넌트 단위의 개발을 다뤘었다. 이런 방식을 사용하는 대표적인 라이브러리가 바로 리엑트다. 이번에는 직접 만들어보려한다.
“What I cannot create, I do not understand.”
예전에 합성생물학을 공부할 때 들었던 말로 내가 만들 수 없다면 이해한 것이 아니라는 말이다. 이미 개발되어있는 라이브러리나 프레임워크라도 사용하기만 한다면 그 의미를 완전히 알 수 없다고 생각한다. 한 번 해보자!
아이디어
자바스크립트는 html을 동적으로 조작하는데 특화된 언어로써 웹을 조작하는 여러 API를 제공하고 있고 자바스크립트는 객체 지향 언어다.
이를 통해 페이지의 각 부분들을 함수로 묶어서 작성하고 모듈 단위로 분리해서 적절히 호출하여 페이지를 구성하는 방법을 생각해볼 수 있다.
가장 상위 요소의 아이디는 root로 했다. 변경해도 되지만 리엑트의 개념을 가져와서 root로 정했다.
// main.js
import App from './App'
const root = document.getElementById('root');
root.append(new App().el);
id가 root인 div요소를 만들고 append메소드를 이용해서 자식요소를 추가한다.
// Component.js
export class Component {
constructor() {
this.render();
}
const element = document.createElement('div');
render() {
el.innerHTML = `
<h1>Hello world</h1>
<p>lorem</p>
`;
};
}
컴포넌트라는 클래스를 만들고 html코드를 작성하고 렌더링시킨다.
지금은 샘플로 하드코딩한 상태다.
// App.js
export default class App extends Component{
const routerView = document.createElement('router-view');
render() {
this.el.append(
)
}
}
컴포넌트를 생성하고 렌더링하는 클래스를 만든다.
생성하려는 태그는 컴포넌트마다 달라지기 때문에 이 부분을 인자로 받아서 사용하면 좋을 것 같고 Component클래스 입장에서는 자식 요소가 어떤 내용을 렌더링할지 알 수 없기 때문에 상속을 활용해서 이를 상속받는 컴포넌트에서 구현하도록한다.
export class Component {
constructor(payload = {}) {
// 구조분해할당
const { tagName = 'div' } = payload
const element = document.createElement
this.render();
}(tagName);
render() {
};
}
만약 헤더를 컴포넌트로 작성한다면 이렇게 할 수 있다.
import { Component } from '../core/Main'
export default class Header extends Component {
constructor() {
super({
tagName: 'header'
});
}
render() {
this.el.innerHTML = /* html */`
<a>Home</a>
<a>menu1</a>
<a>menu2</a>
<a>menu3</a>
`
}
}
// App.js
import { Component } from './core/Core'
import TheHeader from './components/TheHeader'
export default class App extends Component{
const theHeader = new TheHeader().el;
const routerView = document.createElement('router-view');
render() {
this.el.append(
theHeader,
)
}
}
헤더의 메뉴를 클릭하면 거기에 맞는 페이지로 이동해야한다.
페이지 이동은 어떤 식으로 처리할까?
라우터라는 개념을 사용한다.
굳이 다른 페이지로 이동하지 않아도 컴포넌트 방식으로 작업한다면 하나의 페이지인 index.html에서 내부의 컴포넌트를 갈아끼우는 방식으로 페이지를 보여줄 수 있다.
이를 SPA 즉, Single Page Application이라고 부른다.
페이지를 새로고침하지 않아도 url을 변경하고 브라우저의 뒤로가기와 앞으로가기등의 기능을 사용할 수 있다.
라우팅을 하기 위해 router클래스를 작성한다.
// index.js
import { createRouter } from '../core/Core'
import Home from './Home'
export default createRouter([
{ path: '#/', component: Home },
]);
경로를 보면 #기호가 붙어있는데 이건 해시 라우팅이라고 한다.
SPA는 경로가 변하는데 실제 화면은 새로고침되지 않아야한다.
만약 /home과 같이 url을 설정하면 실제 경로가 이동된다. 따라서 브라우저가 url의 해시 부분은 서버에 요청이 가지 않는다. 따라서 http://example.com/#/path
url은 해시 앞의 example.com에만 요청이 가고 뒤의 경로가 home, menu와 같이 변해도 새로고침되지 않으면서 화면에 변화를 줄 수 있다.
백엔드 서버가 없어도 프론트에서 경로와 그 경로에 렌더링될 뷰를 관리해서 따라 다른 뷰를 보여줄 수 있는 방법이다.
만약 페이지가 추가된다면 이렇게 할 수 있다.
export default createRouter([
{ path: '#/', component: Home },
{ path: '#/order', component: Order }
]);
이렇게 작성한 경로와 컴포넌트는 이렇게 사용된다.
function routeRender(routes) {
// 접속할 때 해시 모드가 아니면(해시가 없으면) /#/로 리다이렉트!
if (!location.hash) {
history.replaceState(null, '', '/#/') // (상태, 제목, 주소)
}
const routerView = document.querySelector('router-view')
const [hash, queryString = ''] = location.hash.split('?') // 물음표를 기준으로 해시 정보와 쿼리스트링을 구분
// 1) 쿼리스트링을 객체로 변환해 히스토리의 상태에 저장!
const query = queryString
.split('&')
.reduce((acc, cur) => {
const [key, value] = cur.split('=')
acc[key] = value
return acc
}, {})
history.replaceState(query, '') // (상태, 제목)
// 2) 현재 라우트 정보를 찾아서 렌더링!
const currentRoute = routes.find(route => new RegExp(`${route.path}/?$`).test(hash))
routerView.innerHTML = ''
routerView.append(new currentRoute.component().el)
// 3) 화면 출력 후 스크롤 위치 복구!
window.scrollTo(0, 0)
}
export function createRouter(routes) {
return function () {
window.addEventListener('popstate', () => {
routeRender(routes)
})
routeRender(routes)
}
}
export default class Home extends Component {
render() {
const headline = new Headline().el;
const search = new Search().el;
const movieList = new MovieList().el;
const movieListMore = new MovieListMore().el;
this.el.classList.add('container');
this.el.append(
headline,
search,
movieList,
movieListMore
);
}
}