영화 프로젝트를 시작하기 전, 사용되는 heropy.js에 대해서 알아보겠습니다.
heropy.js는 component,router,store 이렇게 3가지로 구분되어 작성되어 있는데 하나씩 살펴보겠습니다.
살펴보기 전에 하나 알고 가야 하는 지식이 있습니다. 바로 export default입니다.
이것은 위에 글에서 잠깐 나온 말입니다. 바로 module속성을 통해 다른 모듈을 가져올 수 있고, import와 export 문법을 사용할 수 있게 된다고 알고 있습니다.
JavaScript의 export와 default는 ES6 모듈 시스템에서 모듈 간의 코드를 공유하고 사용할 수 있게 하는 중요한 기능입니다. 이 두 개념은 다음과 같은 차이점이 있습니다.
= export 키워드는 모듈에서 특정 변수, 함수, 클래스 등을 외부로 내보낼 때 사용합니다. export된 요소는 다른 모듈에서 import를 통해 사용할 수 있습니다. export는 두 가지 방식으로 사용할 수 있습니다
Named Export (명명된 내보내기)
= 여러 개의 변수나 함수를 각각 내보낼 수 있습니다.
예시
// utils.js
export const PI = 3.14;
export function add(a, b) {
return a + b;
}
사용방법
// main.js
import { PI, add } from './utils.js';
console.log(PI); // 3.14
console.log(add(2, 3)); // 5
모두 내보내기 / Export all
// utils.js
export * from './constants.js';
= export default는 모듈에서 단 하나의 값을 기본으로 내보낼 때 사용합니다. 모듈 당 하나의 default export만 가질 수 있습니다. 이를 사용할 때는 import할 때 중괄호 {}를 생략합니다.
예시
// math.js
export default function multiply(a, b) {
return a * b;
}
사용방법
// main.js
import multiply from './math.js';
console.log(multiply(2, 3)); // 6
정리
export는 여러 개의 변수나 함수를 개별적으로 내보내는 데 사용되며, import 시 중괄호 {}를 사용합니다.
export default는 모듈에서 하나의 기본 값을 내보내는 데 사용되며, import 시 중괄호 {}를 생략합니다.
이제 이 내용을 토대로 한번 3가지를 하나씩 살펴보겠습니다.
export class Component {
constructor(payload = {}) {
const {
tagName = 'div', // 최상위 요소의 태그 이름
props = {},
state = {}
} = payload
this.el = document.createElement(tagName) // 컴포넌트의 최상위 요소
this.props = props // 컴포넌트가 사용될 때 부모 컴포넌트에서 받는 데이터
this.state = state // 컴포넌트 안에서 사용할 데이터
this.render()
}
render() { // 컴포넌트를 렌더링하는 함수
// ...
}
}
이후, createElement는 주어진 tagName을 통해 해당하는 HTML요소를 생성합니다.
생성된 요소는 this.el에 해당하고, this.el은 컴포넌트의 최상위 HTML요소가 됩니다.
this.props = props
this.state = state
this.render()
render()
// 페이지 렌더링!
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)
}
}
이 코드는 간단한 JavaScript로 만들어진 라우터(Router) 시스템입니다. 라우터는 사용자가 웹사이트에서 특정 URL을 방문할 때 어떤 페이지를 보여줄지 결정하는 역할을 합니다. 이 코드에서는 해시 기반 라우팅을 사용하여, 특정 URL 해시(# 뒤에 오는 부분)를 기반으로 페이지를 렌더링하고, 쿼리 문자열을 처리하며, 페이지 이동 시 스크롤 위치를 복구합니다. 각 부분을 하나씩 설명해 보겠습니다.
function routeRender(routes) {
if (!location.hash) {
history.replaceState(null, '', '/#/')
}
const routerView = document.querySelector('router-view')
const [hash, queryString = ''] = location.hash.split('?')
const query = queryString
.split('&')
.reduce((acc, cur) => {
const [key, value] = cur.split('=')
acc[key] = value
return acc
}, {})
history.replaceState(query, '')
const currentRoute = routes.find(route => new RegExp(`${route.path}/?$`).test(hash))
routerView.innerHTML = ''
routerView.append(new currentRoute.component().el)
window.scrollTo(0, 0)
}
현재 라우트 찾기: routes 배열에서 현재 해시와 일치하는 경로를 가진 라우트를 찾습니다. RegExp를 사용해 라우트 경로와 해시가 일치하는지 검사합니다.
라우트 컴포넌트 렌더링: 찾은 라우트에 해당하는 컴포넌트를 생성하고, 그 컴포넌트의 el 요소를 router-view에 추가하여 페이지에 표시합니다.
스크롤 위치 복구: 새로운 페이지가 로드되면, 스크롤 위치를 최상단(0, 0)으로 이동시켜 사용자가 항상 페이지의 맨 위에서 시작할 수 있도록 합니다.
export function createRouter(routes) {
return function () {
window.addEventListener('popstate', () => {
routeRender(routes)
})
routeRender(routes)
}
}
createRouter 함수는 위에서 설명한 routeRender 함수를 페이지 로딩 시 자동으로 실행되도록 설정해줍니다.
즉 이 모든 전체 코드는 매우 기본적인 라우터 시스템을 구현한 것으로, 해시 기반의 라우팅, 쿼리 문자열 처리, 그리고 페이지 렌더링을 담당합니다. 이 시스템을 사용하면 단일 페이지 애플리케이션(SPA)에서 URL 해시를 기반으로 여러 페이지를 쉽게 관리하고, 사용자가 페이지 이동 시 일관된 경험을 제공할 수 있습니다.
export class Store {
constructor(state) {
this.state = {} // 상태(데이터)
this.observers = {}
for (const key in state) {
// 각 상태에 대한 변경 감시(Setter) 설정!
Object.defineProperty(this.state, key, {
// Getter
get: () => state[key],
// Setter
set: val => {
state[key] = val
if (Array.isArray(this.observers[key])) { // 호출할 콜백이 있는 경우!
this.observers[key].forEach(observer => observer(val))
}
}
})
}
}
// 상태 변경 구독!
subscribe(key, cb) {
Array.isArray(this.observers[key]) // 이미 등록된 콜백이 있는지 확인!
? this.observers[key].push(cb) // 있으면 새로운 콜백 밀어넣기!
: this.observers[key] = [cb] // 없으면 콜백 배열로 할당!
}
}
이 코드는 JavaScript에서 상태 관리와 관련된 간단한 Store 클래스를 정의하고 있습니다. Store 클래스는 상태(state)를 관리하고, 상태가 변경될 때 이를 감지하여 등록된 콜백 함수들을 호출하는 기능을 제공합니다. 이는 데이터와 그 데이터에 대한 변화를 관리할 수 있는 일종의 "중앙 저장소" 역할을 합니다. 이 코드를 자세히 설명해드리겠습니다.
export class Store {
constructor(state) {
this.state = {} // 상태(데이터)
this.observers = {}
for (const key in state) {
// 각 상태에 대한 변경 감시(Setter) 설정!
Object.defineProperty(this.state, key, {
// Getter
get: () => state[key],
// Setter
set: val => {
state[key] = val
if (Array.isArray(this.observers[key])) { // 호출할 콜백이 있는 경우!
this.observers[key].forEach(observer => observer(val))
}
}
})
}
for...in 루프: 이 루프는 state 객체의 각 속성을 순회합니다. state 객체는 초기 상태 데이터를 담고 있습니다.
Object.defineProperty: 이 메서드는 객체의 속성에 대한 특정 동작(여기서는 getter와 setter)을 정의합니다.
Getter: get: () => state[key]는 this.state[key]에 접근할 때마다 실제 state[key]의 값을 반환합니다. 즉, this.state.key로 값을 읽을 때 state[key]의 값을 가져옵니다.
Setter: set: val => { ... }는 this.state[key]의 값이 변경될 때 실행됩니다. 이 setter는 상태를 업데이트하고(state[key] = val), 그 상태에 대해 등록된 콜백 함수가 있으면 이를 호출하여 변경된 값을 전달합니다.
콜백 호출: if (Array.isArray(this.observers[key]))는 특정 상태(key)에 대해 등록된 콜백 함수들이 있는지 확인합니다. 콜백 함수들이 배열로 저장되어 있다면, forEach를 사용해 모든 콜백 함수를 호출하고 새로운 값을 전달합니다.
subscribe(key, cb) {
Array.isArray(this.observers[key]) // 이미 등록된 콜백이 있는지 확인!
? this.observers[key].push(cb) // 있으면 새로운 콜백 밀어넣기!
: this.observers[key] = [cb] // 없으면 콜백 배열로 할당!
}
}
이 Store 클래스는 상태 관리와 관련된 기본적인 기능을 제공하여, 상태가 변경될 때 자동으로 등록된 콜백 함수를 호출해 데이터를 동기화합니다. 이렇게 하면 여러 컴포넌트가 동일한 상태를 공유하면서도, 상태 변경 시 자동으로 업데이트를 반영할 수 있습니다.