바닐라-영화소개프로젝트(5)

김희목·2024년 8월 19일
0

패스트캠퍼스

목록 보기
43/53

프로젝트 깃허브

1. Header 컴포넌트

이번에는 모든 페이지의 상단의 보이는 Header 컴포넌트를 만들어 보겠습니다.

구성요소를 보시면 왼쪽상단에는 로고 부분이 있고, 그 옆에는 Search,Movie,About의 페이지로 이동할 수 있는 내비게이션 버튼들이 있습니다. 그리고 화면 오른쪽 상단에는 사용자 이미지가 존재하고 있습니다. 그리고, 스크롤을 내리거나 올렸을 경우, 배경이 반투명하게 보이는 구조로 만들어 보겠습니다.

우리 프로젝트의 컴포넌트 폴더에다가 Header.js 파일을 만들어 작성해보겠습니다.

TheHeader.js

import { Component } from "../core/heropy";

export default class TheHeader extends Component {
  constructor() {
    super({
      tagName: "header",
      state: {
        menus: [
          {
            name: "Search",
            href: "#/",
          },
          {
            name: "Movie",
            href: "#/movie?id=tt4520988",
          },
          {
            name: "About",
            href: "#/about",
          },
        ],
      },
    })
  }
  render() {
    this.el.innerHTML = /* html */ `
      <a href="#/" class="logo"><span>OMDbAPI</span>.COM</a>
      <nav>
        <ul>
          ${this.state.menus
            .map((menu) => {
              return /* html */ `
              <li>
                <a class="${isActive ? "active" : ""}" href="${menu.href}">${
                menu.name
              }</a>
              </li>
            `;
            })
            .join("")}
        </ul>
      </nav>
      <a href="#/about" class="user">
        <img src="https://heropy.blog/css/images/logo.png" alt="User" />
      </a>
    `;
  }
}

해당 코드를 살펴보자면, 우선 tagName: header을 생성하고, 초기 state를 설정하고, menus 배열은 세 가지 내비게이션 항목의 정보를 포함하고 있습니다.

render 메서드에서는 html의 하위요소로 컴포넌트의 내용을 구성하고 있습니다. 구성내용으로 a태그로 logo이미지를 가져와서 메인페이지로 연결되게 만들어 놓았고, nav태그를 사용하여 ul태그로 덮고, map함수를 이용하여 li태그와 a태그를 생성하여 3가지 내비게이션을 출력하게 만들었습니다.

해당 내비게이션 항목은 menus 배열에서 가져온 정보를 활용하여 동적으로 생성됩니다.

마지막으로 사용자 이미지를 a태그로 작성하여, about페이지로 연결되도록 구성하였습니다. 이후 css 디자인을 통해
스크롤을 내렸을때, 불투명하게 만들도록 디자인 하고, 해당 위치에 있도록 작성해주시면 됩니다.

이렇게 만들어 놓은 header 컴포넌트를 App.js에 연결하여 화면에 보여지도록 하겠습니다.

App.js

import { Component } from "./core/heropy";
import TheHeader from "./components/TheHeader";

export default class App extends Component {
  render() {
    const theHeader = new TheHeader().el
    const routerView = document.createElement('router-view')

    this.el.append(theHeader,routerView)
  }
}

헤더 컴포넌트를 import로 가져와서 변수에 요소에 가져오고, 해당 변수를 append하여 화면에 보여지도록 했습니다.

그러면 이번에는 헤더 부분에 현재 페이지의 맞게 네비게이션 버튼이 활성화 될 수 있는 구조로 변경해보겠습니다.

TheHeader.js

import { Component } from "../core/heropy";

export default class TheHeader extends Component {
  constructor() {
    super({
      tagName: "header",
      state: {
        menus: [
          {
            name: "Search",
            href: "#/",
          },
          {
            name: "Movie",
            href: "#/movie?id=tt4520988",
          },
          {
            name: "About",
            href: "#/about",
          },
        ],
      },
    })
    window.addEventListener('popstate', () => {
      this.render()
    })
  }
  render() {
    this.el.innerHTML = /* html */ `
      <a href="#/" class="logo"><span>OMDbAPI</span>.COM</a>
      <nav>
        <ul>
          ${this.state.menus
            .map(menu => {
              const href = menu.href.split("?")[0];
              const hash = location.hash.split("?")[0];
              const isActive = href === hash;
              return /* html */ `
              <li>
                <a class="${isActive ? "active" : ""}" href="${menu.href}">${
                menu.name
              }</a>
              </li>
            `;
            })
            .join("")}
        </ul>
      </nav>
      <a href="#/about" class="user">
        <img src="https://heropy.blog/css/images/logo.png" alt="User" />
      </a>
    `;
  }
}

추가한 코드들 부터 확인해보겠습니다.

    const href = menu.href.split("?")[0];
              const hash = location.hash.split("?")[0];
              const isActive = href === hash;

작성한 map 함수 위에 이런식으로 작성이 되어 있습니다. 우선 href변수에 현재 메뉴의 href 정보들을 해쉬태그에서 ? 앞에 있는 정보들을 담는데, 그 이유는 href: "#/movie?id=tt4520988" 즉 movie의 href는 어떤 영화를 선택했는지에 따라 id의 값이 매번 바뀌고, 그 정보가 현재 페이지가 맞는지 확인하는데는 불필요하기 때문에 query string 내용을 제거하는 것이고, hash 변수에는 현재 href 정보를 가져와서 isActive 변수에다가 두 개의 정보가 같은 정보일경우를 확인하는 코드입니다.

 <a class="${isActive ? "active" : ""}" href="${menu.href}">

그리고 a 클래스에 삼항연산자를 이용하여, 현재 같은 주소를 바라보고 있을경우 active라는 클래스를 추가해주는데 이는 css를 통해서 같은 주소를 바라보고 있는경우 활성화시키는 코드를 css에 추가하기 위함입니다.

 window.addEventListener('popstate', () => {
      this.render()

그리고 우리는 이러한 상태들이 변경되는 시점마다 즉, 페이지가 바뀔 때마다 render를 재실행 시켜서 각각의 메뉴와 현재 페이지의 주소를 비교해서 활성화 시켜주는 역할입니다.


header 컴포넌트를 만들었으니, 이번에는 하단에 위치할 footer 컴포넌트를 작성해 보겠습니다.

footer에는 github 주소와, 블로그 주소를 넣어 실제로 클릭을 할 경우 해당 주소로 이동할 수 있게 만들어보겠습니다.

TheFooter.js

import { Component } from "../core/heropy";

export default class TheFooter extends Component {
  constructor() {
    super({
      tagName: "footer",
    });
  }

  render() {
    this.el.innerHTML = /* html */ `
      <div>
        <a href="${repository}">
          GitHub Repository
        </a>
      </div>
      <div>
        <a href="${github}">
          ${new Date().getFullYear()}
          KIMHEEMOK
        </a>
      </div>
    `;
  }
}

마찬가지로, tagName을 footer로 작성하고, 요소를 생성해준 다음, 하위 요소로 a태그를 2개를 만들어 각각의 이름과 href를 입력하여 만들어 줍니다.

그리고 css를 작성하고 마찬가지로 App.js에 연결하여 화면에 보여지게 만들어 주시면 됩니다.

이번에는 About 페이지를 만들건데, footer의 깃헙주소나, 블로그 주소는 About 페이지에도 존재하기 때문에 두 state를 연결하여 같이 사용할 수 있도록 나중에 추가해보겠습니다.


3. 내정보 페이지 컴포넌트

About 페이지를 만들어 볼건데 해당 구조를 먼저 파악해보겠습니다.

우선 사용자 이미지가 보이고, 그다음 닉네임, 이름, 이메일주소, 깃헙주소, 블로그주소가 있습니다.
routes 폴더에 About.js 파일을 만들어 작성해 보겠습니다.

About.js

import { Component } from "../core/heropy";
import aboutStore from "../store/about";

export default class About extends Component {
  render() {
    const { photo, name, email, github, blog } = aboutStore.state;
    this.el.classList.add("container", "about");
    this.el.innerHTML = /* html */ `
      <div style="background-image: url(${photo});" class="photo"></div>
      <p class="name">${name}</p>
      <p><a href="https://mail.google.com/mail/?view=cm&fs=1&to=${email}" target="_blank">${email}</a></p>
      <p><a href="${github}" target="_blank">GitHub</a></p>
      <p><a href="${blog}" target="_blank">BLOG</a></p>
    `;
  }
}

해당 코드를 보면 aboutStore.state를 통해 정보를 가져와서 사용하는 것을 볼 수 있습니다. 이처럼 Footer에서 사용한 정보들을 따로 store을 통해 관리하면 여러 컴포넌트에서 공통적으로 사용이 가능하고, 관리도 용이하기 때문에 이처럼 사용할 수 있습니다.

store 폴더에 about.js라는 파일을 만들어주세요.

about.js

import { Store } from "../core/heropy";

export default new Store({
  photo: 'https://heropy.blog/css/images/logo.png',
  name: 'HEROPY / KMHEEMOK',
  email: 'gmlahr970516@naver.com',
  blog: 'https://velog.io/@ksh4704/posts',
  github: 'https://github.com/kjd43871',
  repository: 'https://github.com/kjd43871/vanillajs-movie-app'
})

이런식으로 해당 state를 store를 통해 저장하여 관리하면 해당 파일에서 정보만 바뀌어야 할때, 수정을 하면 about 컴포넌트와 Footer 컴포넌트에서 수정하지 않고 한번에 관리할 수 있게 됩니다.

TheFooter.js

import { Component } from "../core/heropy";
import aboutStore from '../store/about'

export default class TheFooter extends Component {
  constructor() {
    super({
      tagName: "footer",
    });
  }

  render() {
    const { github, repository} = aboutStore.state
    this.el.innerHTML = /* html */ `
      <div>
        <a href="${repository}">
          GitHub Repository
        </a>
      </div>
      <div>
        <a href="${github}">
          ${new Date().getFullYear()}
          KIMHEEMOK
        </a>
      </div>
    `;
  }
}

위에서 작성한 footer 컴포넌트와 about 컴포넌트를 확인해보면, aboutStore를 가져와서 객체 구조 분해 할당을 통해 내용을 가져와 템플릿리터럴을 통해 쉽게 적용시킬 수 있습니다.

이제 적용시킨 about 페이지를 index.js 파일에 연결하여 페이지 이동이 가능할 수 있게 작성해보겠습니다.

index.js

import { createRouter } from "../core/heropy";
import Home from "./Home";
import Movie from "./Movie";
import About from './About'

export default createRouter([
  { path: "#/", component: Home },
  { path: "#/movie", component: Movie },
  { path: "#/about", component: About},
]);

이제 실제로 적용해야 할 페이지나, 기능들은 모두 구현을 완료했습니다.
이번에는 이슈를 정리하고, 추가적으로 필요한 내용들을 추가하고, 마무리 해보겠습니다.

4. 이슈 정리 및 보안

만약 사용자가 프로젝트에 구성되지 않은 페이지로 이동을 할 경우의 이슈를 해결하기 위해 보안해보도록 하겠습니다.

routes 폴더에 NotFound.js 파일을 만들어 작성해보겠습니다.

NotFound.js

import { Component } from "../core/heropy";

export default class NotFound extends Component {
  render() {
    this.el.classList.add('container', 'not-found')
    this.el.innerHTML = /* html */ `
      <h1>
        Sorry..<br>
        Page Not Found.
      </h1>
    `
  }
}

이렇게 작성해준 뒤 index.js에도 작성해주겠습니다.

index.js

import { createRouter } from "../core/heropy";
import Home from "./Home";
import Movie from "./Movie";
import About from './About'
import NotFound from './NotFound'

export default createRouter([
  { path: "#/", component: Home },
  { path: "#/movie", component: Movie },
  { path: "#/about", component: About},
  { path: '.{0,}', component:NotFound}
]);

해당 코드를 보면 heropy 파일에서 creatRouter 함수를 가져와서 사용하는데 해당 코드를 잠깐 살펴보겠습니다.

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)
  }
}

여기서 2)현재 라우트 정보를 찾아서 렌더링 부분을 보시면 routes 옵션으로 받은 내용을 find 메소드를 이용하여 찾게 되는데 find 메소드의 경우 어떤 결과를 찾게 되면 바로 반복을 종료하고 찾을 결과를 반환합니다.

이러한 정보들을 통해 다시 Index.js 파일을 살펴보면, 우선 메인 페이지의 해당하는 내용을 찾았다면, 해당 객체 데이터를 반환하고 나머지 내용은 살펴보지 않습니다. 영화 상세 정보 페이지로 이동을 했다면, 첫번째 내용을 확인하고 다른점을 확인했다면 바로 아래 moive 정보를 확인하고 일치하면 해당 객체 데이터를 반환하고 나머지 정보들은 확인하지 않습니다.

이를 통해 about페이지 까지 확인해도 일치하는 정보가 없다면 Page Not Found 페이지를 호출하여 보여주게 됩니다. 이처럼 우리가 create router 부분에다가 사용할 페이지의 정보를 순서대로 적어주고 제일 마지막 부분에다가 페이지를 찾을 수 없을 때에 해당하는 내용을 작성을 해줘야만 기본적인 내용이 일치하고 내려오게 되면 이제 모든 페이지의 주수와 일치할 수 있는 경로의 정규 표현식 내용을 작성했기 때문에 결국 제일 마지막까지 도착하면 page not found를 보여줄 수 있게 됩니다.

0개의 댓글