바닐라-영화소개프로젝트(heropy.js)

김희목·2024년 8월 11일
0

패스트캠퍼스

목록 보기
37/53

영화 프로젝트를 시작하기 전, 사용되는 heropy.js에 대해서 알아보겠습니다.

1. heropy.js를 사용하여 프로젝트 진행

heropy.js는 component,router,store 이렇게 3가지로 구분되어 작성되어 있는데 하나씩 살펴보겠습니다.

살펴보기 전에 하나 알고 가야 하는 지식이 있습니다. 바로 export default입니다.

이것은 위에 글에서 잠깐 나온 말입니다. 바로 module속성을 통해 다른 모듈을 가져올 수 있고, import와 export 문법을 사용할 수 있게 된다고 알고 있습니다.

JavaScript의 export와 default는 ES6 모듈 시스템에서 모듈 간의 코드를 공유하고 사용할 수 있게 하는 중요한 기능입니다. 이 두 개념은 다음과 같은 차이점이 있습니다.

1. export

= 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';

2. export default

= 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가지를 하나씩 살펴보겠습니다.

1. Component

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() { // 컴포넌트를 렌더링하는 함수
    // ...
  }
}

1. export class Component

  • export 키워드를 사용하여 이 클래스를 다른 파일에서도 사용할 수 있게 내보내 줍니다.
  • Component는 클래스로 작성하여 이 클래스를 기반으로 여러 개의 컴포넌트를 만들 수 있게 해줍니다.

2. constructor(payload = {})

  • 클래스의 constructor은 객체가 생성될 때 호출되는 특별한 함수입니다.
  • 여기서 payload라는 매개변수를 받는데, 기본값으로 빈 객체를 할당합니다.
  • payload는 객체로서, 컴포넌트를 만들 때 필요한 여러 정보를 담아 전달하는 역할입니다.

3. const { tagName = 'div', props = {}, state = {} } = payload

  • 이 부분은 payload 객체를 비구조화 할당을 통해 tagName과 props,state라는 세 가지 변수를 만듭니다.
  • tagName은 생성할 html 요소의 태그 이름입니다. 기본값은 div로 할당했습니다.
  • props는 부모 컴포넌트에서 이 컴포넌트에 전달되는 데이터 입니다. 기본값은 빈 객체를 할당했습니다.
  • state는 이 컴포넌트 내부에서 관리되는 상태 데이터입니다. 기본값은 빈 객체를 할당했습니다.

이후, createElement는 주어진 tagName을 통해 해당하는 HTML요소를 생성합니다.
생성된 요소는 this.el에 해당하고, this.el은 컴포넌트의 최상위 HTML요소가 됩니다.

this.props = props

  • props를 this.props에 저장하여, 나중에 컴포넌트 내부에서 부모로부터 받은 데이터를 사용할 수 있게 합니다.

this.state = state

  • state를 this.state에 저장합니다. 이 데이터는 컴포넌트 내부에서 사용할 상태 정보입니다. 이 상태 정보는 나중에 컴포넌트가 업데이트될 때 사용됩니다.

this.render()

  • constructor 마지막에서 this.render()를 호출합니다. render()는 컴포넌트를 실제로 화면에 그리는 역할을 합니다.
  • render() 함수는 나중에 사용자에 의해 정의될 수 있으며, 여기에서는 빈 함수로 정의되어 있습니다.

render()

  • 이 함수는 Component 클래스를 상속한 자식 클래스에서 재정의(오버라이딩)되어, 컴포넌트를 구체적으로 어떻게 화면에 표시할지 정의합니다.

2. routers

// 페이지 렌더링!
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 해시(# 뒤에 오는 부분)를 기반으로 페이지를 렌더링하고, 쿼리 문자열을 처리하며, 페이지 이동 시 스크롤 위치를 복구합니다. 각 부분을 하나씩 설명해 보겠습니다.

1. 기본적인 라우팅 흐름 (routeRender 함수)

function routeRender(routes) {
  if (!location.hash) {
    history.replaceState(null, '', '/#/') 
  }
  const routerView = document.querySelector('router-view')
  const [hash, queryString = ''] = location.hash.split('?') 
  • routeRender 함수는 페이지가 로드되거나 URL이 변경될 때 호출되어, 올바른 페이지를 렌더링하는 핵심 함수입니다.
  • 해시 모드 확인: 사용자가 웹사이트에 접속할 때, URL에 #이 포함되어 있지 않으면 자동으로 /#/로 리다이렉트됩니다. 이는 모든 URL이 해시를 포함하도록 보장하는 역할을 합니다.
  • DOM 요소 선택: 페이지에서 router-view라는 특수한 요소를 찾아서, 이 요소에 페이지 내용을 동적으로 삽입합니다.
  • 해시와 쿼리 스트링 분리: location.hash.split('?')를 사용해 URL 해시와 쿼리 문자열을 분리합니다. 예를 들어, #/home?user=123이라는 URL이 있다면 #/home과 user=123으로 나뉩니다.

2. 쿼리 문자열 처리

const query = queryString
    .split('&')
    .reduce((acc, cur) => {
      const [key, value] = cur.split('=')
      acc[key] = value
      return acc
    }, {})
  history.replaceState(query, '') 
  • 쿼리 문자열 객체 변환: 쿼리 문자열을 & 기준으로 분리(split)하고, 각각의 키-값 쌍을 객체로 변환합니다(reduce). 예를 들어, user=123&name=John이라는 쿼리 문자열이 있다면 { user: '123', name: 'John' }으로 변환됩니다.
  • 히스토리 상태에 저장: 변환된 쿼리 객체를 history.replaceState를 통해 브라우저의 히스토리 상태로 저장합니다. 이 상태는 나중에 페이지를 다시 방문하거나 뒤로 가기를 했을 때 복구할 수 있습니다.

3. 현재 라우트 정보에 맞는 컴포넌트 렌더링

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)으로 이동시켜 사용자가 항상 페이지의 맨 위에서 시작할 수 있도록 합니다.

4. 라우터 생성 및 이벤트 연결 (createRouter 함수)

export function createRouter(routes) {
  return function () {
    window.addEventListener('popstate', () => {
      routeRender(routes)
    })
    routeRender(routes)
  }
}

createRouter 함수는 위에서 설명한 routeRender 함수를 페이지 로딩 시 자동으로 실행되도록 설정해줍니다.

  • 이벤트 리스너 등록: popstate 이벤트는 사용자가 브라우저에서 뒤로 가기, 앞으로 가기 버튼을 클릭할 때 발생합니다. 이 이벤트가 발생할 때마다 routeRender 함수를 호출하여 올바른 페이지를 렌더링합니다.
  • 초기 렌더링: createRouter가 반환하는 함수가 처음 호출될 때, 즉 페이지가 처음 로드될 때도 routeRender를 호출하여 페이지를 렌더링합니다.

즉 이 모든 전체 코드는 매우 기본적인 라우터 시스템을 구현한 것으로, 해시 기반의 라우팅, 쿼리 문자열 처리, 그리고 페이지 렌더링을 담당합니다. 이 시스템을 사용하면 단일 페이지 애플리케이션(SPA)에서 URL 해시를 기반으로 여러 페이지를 쉽게 관리하고, 사용자가 페이지 이동 시 일관된 경험을 제공할 수 있습니다.

3. Store

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)를 관리하고, 상태가 변경될 때 이를 감지하여 등록된 콜백 함수들을 호출하는 기능을 제공합니다. 이는 데이터와 그 데이터에 대한 변화를 관리할 수 있는 일종의 "중앙 저장소" 역할을 합니다. 이 코드를 자세히 설명해드리겠습니다.

1. 클래스 정의 및 생성자 함수 (constructor)

export class Store {
  constructor(state) {
    this.state = {} // 상태(데이터)
    this.observers = {}
  • Store 클래스 정의: Store 클래스는 상태 관리 기능을 제공하는 클래스로, 여러 컴포넌트가 공유하는 데이터를 중앙에서 관리할 수 있게 해줍니다.
  • constructor(state): 이 생성자 함수는 state라는 객체를 매개변수로 받아, 이 객체의 각 속성에 대해 getter와 setter를 정의합니다. this.state는 상태를 저장하고 관리하는 객체이며, this.observers는 특정 상태가 변경될 때 실행할 콜백 함수들을 저장하는 객체입니다.

2. 상태에 대한 Getter와 Setter 정의

  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를 사용해 모든 콜백 함수를 호출하고 새로운 값을 전달합니다.

3. 상태 변경 구독 메서드 (subscribe)

 subscribe(key, cb) {
    Array.isArray(this.observers[key]) // 이미 등록된 콜백이 있는지 확인!
      ? this.observers[key].push(cb) // 있으면 새로운 콜백 밀어넣기!
      : this.observers[key] = [cb] // 없으면 콜백 배열로 할당!
  }
}
  • subscribe(key, cb): 이 메서드는 특정 상태(key)가 변경될 때 실행할 콜백 함수(cb)를 등록하는 역할을 합니다.
  • 콜백 배열 관리:
    • Array.isArray(this.observers[key]): 특정 상태(key)에 대해 이미 등록된 콜백 함수들이 있는지 확인합니다.
  • 콜백 추가:
    • 등록된 콜백이 이미 존재하는 경우(true), 새 콜백 함수(cb)를 기존 콜백 배열에 추가(push)합니다.
    • 등록된 콜백이 존재하지 않는 경우(false), 해당 상태에 대한 새로운 콜백 배열을 생성하고, 그 배열에 콜백 함수(cb)를 할당합니다.

결론

이 Store 클래스는 상태 관리와 관련된 기본적인 기능을 제공하여, 상태가 변경될 때 자동으로 등록된 콜백 함수를 호출해 데이터를 동기화합니다. 이렇게 하면 여러 컴포넌트가 동일한 상태를 공유하면서도, 상태 변경 시 자동으로 업데이트를 반영할 수 있습니다.

0개의 댓글