사건의 전말
- 바닐라 js로 spa router를 만들던 중 서버를 구축하는 걸 봤다.
- 왜 서버를 구축하지? koa에 router를 쓰던데 이것때문인가 했다.
- 하지만 알고보니 html, js 파일을 띄우려면 서버란 건 반드시 필요한 존재였다.
여기서 서버가 어떻게 사용되는지 설명해보자면,
- 브라우저의 요청은 무조건 서버로 들어온다.
- 사용자가 주소창을 클릭하고 엔터치거나 새로고침 하는경우 브라우저는 그 url을 바탕으로 서버요청을 날리는 것이다.
- 그 경로로 오는 요청은 서버에서 index.html을 다시 던져주고 그 url을 받아서 js가 다시 라우팅을 하는 방식이다.
나는 vscode의 live server로 항상 서버를 띄웠다. 근데
왜 live server를 쓰지 않고 node 기반 프레임워크를 사용하여 서버를 띄울까?
라는 궁금증이 생겼다.- live server와 node server의 차이
- HTML preview를 보고 싶은거라면 차이가 없다.
- 하지만 production으로 사용할 수 없듯이 node 패키지를 설치하면 더 많은 기능을 사용할 수 있다.
- url 공유
- 동기화
- 번들링 도구를 브라우저 친화적으로 분석
결국 서버는 내가 작성한 파일을 띄우려면 어디에든 필요했고, 서버를 어떻게 띄우냐에 차이만 있을 뿐이었다. 전통적인 a 태그 조차도 link tag(
<a href="/service.html">Service</a>
등)을 클릭하면 href 어트리뷰트 값인 리소스 경로가 URL의 path에 추가되어 주소창에 나타나고 해당 리소스를 서버에 요청하는 방식이다.그 이후엔
- 서버는 html로 화면을 표시하는데 부족함이 없는 완전한 리소스를 클라이언트에 응답한다.
- 브라우저는 서버가 응답한 html을 응답받아 렌더링한다(서버 사이드 렌더링SSR).
- 이때 응답받은 html로 전체 페이지를 다시 렌더링하게 되므로 새로고침이 발생한다.
모든 프로그램 구동엔 서버가 필요하다는 건 알았다. 그러면 라우터를 만들 때 live server가 아닌 node server를 사용한 이유는 뭘까? 추측하자면 백엔드 프레임워크를 사용하면 라우터를 구현하기 좀 더 용이한 것 같다. 라이브러리로 router를 제공해주기 때문. 한 가지 분명한 건 프레임워크, 라이브러리 없이도 routing을 만들 수 있다는 것이다. 그럼 여기서 또 선택을... 해야 한다.
다만 라이브러리나 프레임워크를 사용하지 못하는 상황을 대비해 두 가지 방식 모두 알아야 하는 것 같다. 우선 JS로 먼저 구현해보려 한다. (JS를 공부하고 싶은 마음이 우선이기 때문..)
여기서 잠깐 깨달은 바를 적자면, 강의에서 node 서버를 구동한 건 router를 사용하기 위함보단 “우선 서버가 필요하기 때문”이 아니었나 생각이 든다. 그리고 SSR를 하냐, CSR을 하냐를 선택함에 따라 구현 방법이 달라졌던 것이고...🙄
라우팅 만들기
JS에서 URL 라우팅을 처리할 수 있는 간단한 방법은
location.pathname
을 이용해 분기 처리를 시키는 것이다.const { pathname } = location if(pathname === '/') { //home page render } else if (pathname.indexOf('/products/') === 0) { const [, , productId] = pathname.split('/') } else if (pathname === '/cart') { //cart page render }
URL 라우팅의 책임은 App에 있고, 이에 상응하는 동작에 맞게 페이지를 렌더링하는 구조이다. 이렇게 되면 각 페이지는 서로에 대한 의존성 없이 동작할 수 있다.
이때 spa는 index.html을 제외하고 다른 html을 갖지 않기 때문에 404 에러가 날 수 있다. live server setting 값에 index.html을 추가하자.


이제 url을 직접 입력하지 않고 이동할 수 있게 버튼을 만들어 보자. 화면 새로고침 없이 다른 페이지로 이동 처리를 하려면 다음을 기억하자.
- 이동할 페이지 URL을
histroy.pushState
를 통해 변경한다. - 커스텀 이벤트를 사용해 router를 제작한다.
- App.js의 this.route 함수를 실행시킨다.
const ROUTE_CHANGE_EVENT = 'ROUTE_CHANGE'; const { addEventListener, dispatchEvent } = window; export const init = (onRouteChange) => { addEventListener(ROUTE_CHANGE_EVENT, () => { onRouteChange(); }); }; export const routeChange = (url) => { history.pushState(null, null, url); dispatchEvent(new CustomEvent(ROUTE_CHANGE_EVENT)); };
routeChange
가 발생하면pushState
로 url을 이동시킨 후 custom event를 발생시킨다.
init
함수는 ROUTE_CHANGE 이벤트 발생 시this.route
함수를 호출한다.
import { routeChange } from '../router.js'; export default function nav({ $target }) { const $page = document.createElement('div'); $page.className = 'nav'; $page.innerHTML = ` <button id='/'>home</button> <button id='/album'>album</button> <button id='/search'>search</button> `; this.render = () => { $target.appendChild($page); }; $page.addEventListener('click', (e) => { const { id } = e.target; if (id) { routeChange(id); } }); }

추가 1) 뒤로가기 처리가 필요하다면 다음과 같이 뒤로가기, 앞으로 가기 시 발생하는 popState를 삽입하면 된다.
window.addEventListener('popstate', this.route)
추가 2) initialState에 대하여
- initialState를 사용하는 이유 특정 컴포넌트에서 initialState를 사용하는 이유는, `해당 컴포넌트의 동작에 필요한 데이터가 다른 컴포넌트에서 영향을 받아 변경될 수 있을 때, 그 변경사항을 추적할 수 있도록 하기 위함`이라고 생각합니다.예전에 지은 멘토님께서는 컴포넌트가 다루는 데이터의 범위를 생각해보면 그 데이터의 관리에 책임이 있는 컴포넌트의 위치를 알 수 있다고 말씀하셨습니다. 이 말씀을 좀 더 생각해 보니, 저는 컴포넌트를 만들 때 컴포넌트에서 다루는 데이터가 순수하게 컴포넌트 내부에서만 생성되고 유지되는 것이 아니라, 다른 컴포넌트들에 의해 조작될 수 있는 데이터라면 공통의 부모컴포넌트가 해당 데이터에 대한 책임이 있다고 생각이 들더라구요.그래서 만약에 내가 구현하려는 컴포넌트가 다른 컴포넌트에서도 사용되는 데이터에 영향을 준다, 또는 해당 데이터가 다른 컴포넌트에서 영향을 받을 수 있다라고 한다면 해당 데이터의 관리 책임을 지니고 있는 상위 컴포넌트에서 업데이트된 데이터를 받아야 하기 때문에 initialState를 사용하는 것 같아요.
- 내부에서만 필요한 상태를 App.js 에서 관리할 필요가 있을까요? App.js 에서 업데이트 된 데이터를 컴포넌트에 내려주어야 하는 상황이 아니라 컴포넌트의 자체 상태만으로 충분하다면 굳이 헤더 컴포넌트의 내부 상태를 App.js에서 관리할 필요는 없다고 생각합니다.
참고 자료: