[JS] 프레임워크 없이 SPA 만들기 - 2 (view)
![[JS] 프레임워크 없이 SPA 만들기 -2 (view) 글의 썸네일"](/_next/image?url=%2Fassets%2Fimages%2Fthumbnails%2Fjs.png&w=2048&q=75)
소스코드
ChangJuneKim/vaniila-spa at router-1 (github.com) 이전 글 까지 소스코드
ChangJuneKim/vaniila-spa at view-1 (github.com) 현재 글까지 소스코드
이전 글에서 기본적인 router를 구현해봤다. 이번 포스팅에서는 각 경로에 맞는 간단한 화면을 렌더링 해보자.

CSS 소스 코드
header {
background: #3e70d9;
color: white;
font-weight: bold;
height: 75px;
left: 0;
position: fixed;
right: 0;
top: 0;
}
nav {
height: 100%;
}
.nav__link {
align-items: center;
color: #fff;
display: inline-flex;
height: 100%;
padding: 1rem;
text-decoration: none;
}
.nav__link:hover {
background: rgba(255, 255, 255, 0.05);
}
a {
color: #009579;
}
h1, p {
margin: 0;
}
main {
height: 100%;
padding: 1rem;
}
body {
background: #EEE;
padding-top: 75px;
}
body, html {
height: 200%;
}
* {
box-sizing: border-box;
}
header {
background: #3e70d9;
color: white;
font-weight: bold;
height: 75px;
left: 0;
position: fixed;
right: 0;
top: 0;
}
nav {
height: 100%;
}
.nav__link {
align-items: center;
color: #fff;
display: inline-flex;
height: 100%;
padding: 1rem;
text-decoration: none;
}
.nav__link:hover {
background: rgba(255, 255, 255, 0.05);
}
a {
color: #009579;
}
h1, p {
margin: 0;
}
main {
height: 100%;
padding: 1rem;
}
body {
background: #EEE;
padding-top: 75px;
}
body, html {
height: 200%;
}
* {
box-sizing: border-box;
}
참고한 영상에서는 템플릿 리터럴을 이용해서 html을 생성했지만, 본 글에서는 조금 번거롭더라도 DOM 메서드들을 사용해서 페이지를 작성해보겠다.
#View 구성하기
#home.js
작성하기
export const home = () => {
// 1) 브라우저의 title을 설정
document.title = "홈페이지";
// 2) 우리가 작성한 home 함수는 돔을 생성하는 getHTML 함수를 리턴한다.
const getHTML = async () => {
const main = document.createElement("main");
const title = document.createElement("h1");
title.textContent = "홈";
main.appendChild(title);
const subTitle = document.createElement("h2");
subTitle.textContent = "바닐라 JS로 SPA 만들기";
main.appendChild(subTitle);
const description = document.createElement("p");
description.textContent = "Lorem ipsum ... Aut harum iste quia";
main.appendChild(description);
const links = document.createElement("ul");
const postsLink = document.createElement("li");
const postsLinkAnchor = document.createElement("a");
postsLinkAnchor.setAttribute("href", "/posts");
postsLinkAnchor.setAttribute("data-link", "");
postsLinkAnchor.textContent = "최근 게시물 보기";
postsLink.appendChild(postsLinkAnchor);
links.appendChild(postsLink);
const settingsLink = document.createElement("li");
const settingsLinkAnchor = document.createElement("a");
settingsLinkAnchor.setAttribute("href", "/settings");
settingsLinkAnchor.setAttribute("data-link", "");
settingsLinkAnchor.textContent = "설정 페이지";
settingsLink.appendChild(settingsLinkAnchor);
links.appendChild(settingsLink);
main.appendChild(links);
return main;
};
return {
getHTML,
};
};
export const home = () => {
// 1) 브라우저의 title을 설정
document.title = "홈페이지";
// 2) 우리가 작성한 home 함수는 돔을 생성하는 getHTML 함수를 리턴한다.
const getHTML = async () => {
const main = document.createElement("main");
const title = document.createElement("h1");
title.textContent = "홈";
main.appendChild(title);
const subTitle = document.createElement("h2");
subTitle.textContent = "바닐라 JS로 SPA 만들기";
main.appendChild(subTitle);
const description = document.createElement("p");
description.textContent = "Lorem ipsum ... Aut harum iste quia";
main.appendChild(description);
const links = document.createElement("ul");
const postsLink = document.createElement("li");
const postsLinkAnchor = document.createElement("a");
postsLinkAnchor.setAttribute("href", "/posts");
postsLinkAnchor.setAttribute("data-link", "");
postsLinkAnchor.textContent = "최근 게시물 보기";
postsLink.appendChild(postsLinkAnchor);
links.appendChild(postsLink);
const settingsLink = document.createElement("li");
const settingsLinkAnchor = document.createElement("a");
settingsLinkAnchor.setAttribute("href", "/settings");
settingsLinkAnchor.setAttribute("data-link", "");
settingsLinkAnchor.textContent = "설정 페이지";
settingsLink.appendChild(settingsLinkAnchor);
links.appendChild(settingsLink);
main.appendChild(links);
return main;
};
return {
getHTML,
};
};
#router.js
수정
// 이전 코드...
export const router = async () {
const routes = [
{ path: "/", view: home },
// 다른 경로들 ...
];
// 이전 코드들 ...
const { getHTML } = match.view(); // 1)
const page = await getHTML();
const root = document.querySelector("#root");
// 2)
while (root.firstChild) {
root.removeChild(root.firstChild);
}
// 3)
document.querySelector("#root").appendChild(page);
}
// 이전 코드...
export const router = async () {
const routes = [
{ path: "/", view: home },
// 다른 경로들 ...
];
// 이전 코드들 ...
const { getHTML } = match.view(); // 1)
const page = await getHTML();
const root = document.querySelector("#root");
// 2)
while (root.firstChild) {
root.removeChild(root.firstChild);
}
// 3)
document.querySelector("#root").appendChild(page);
}
home.js
에서 작성한home
함수를routes
객체 배열에view
값으로 넣어준다. 그럼 path와 매치된 페이지 돔 엘리먼트를 리턴하는getHTML
함수를 얻을 수 있다.root
에 이전에 심어져있던 요소들을 모두 지우고 ( 이 코드가 없으면 이전 페이지 아래에 계속 붙어서 생성된다.innerHTML
으로 처리하면 필요없는 코드)- 새롭게
root
에 새로운 요소(getHTML
으로 얻은page
돔 덩어리)를 심어준다.
#다른페이지들 작성
#not-found.js
export const notfound = () => {
document.title = "404 not found";
const getHTML = async () => {
const fragment = document.createDocumentFragment(); // 1)
const title = document.createElement("h1");
title.textContent = "404 not found";
fragment.appendChild(title);
const description = document.createElement("p");
description.textContent =
"Lorem ipsum dolor sit amet, consectetur adipisicing elit. At dolores eius ipsa labore laborum magni non, quae recusandae reprehenderit repudiandae sapiente similique tempore ullam veniam veritatis? Aut harum iste quia";
fragment.appendChild(description);
const homeLinkWrapper = document.createElement("p");
const homeLink = document.createElement("a");
homeLink.setAttribute("href", "/");
homeLink.setAttribute("data-link", "");
homeLink.textContent = "홈으로";
homeLinkWrapper.appendChild(homeLink);
fragment.appendChild(homeLinkWrapper);
return fragment;
};
return {
getHTML,
};
};
export const notfound = () => {
document.title = "404 not found";
const getHTML = async () => {
const fragment = document.createDocumentFragment(); // 1)
const title = document.createElement("h1");
title.textContent = "404 not found";
fragment.appendChild(title);
const description = document.createElement("p");
description.textContent =
"Lorem ipsum dolor sit amet, consectetur adipisicing elit. At dolores eius ipsa labore laborum magni non, quae recusandae reprehenderit repudiandae sapiente similique tempore ullam veniam veritatis? Aut harum iste quia";
fragment.appendChild(description);
const homeLinkWrapper = document.createElement("p");
const homeLink = document.createElement("a");
homeLink.setAttribute("href", "/");
homeLink.setAttribute("data-link", "");
homeLink.textContent = "홈으로";
homeLinkWrapper.appendChild(homeLink);
fragment.appendChild(homeLinkWrapper);
return fragment;
};
return {
getHTML,
};
};
모든 페이지가 형태가 똑같기 때문에 나머지 코드 내용들은 접은 글에 작성해놓았다. 하지만, 학습을 위해서 404 페이지는document.createDocumentFragment()
를 이용해보았다.
document.createDocumentFragment()
는 리액트의 React.fragment(<></>)
와 유사한 기능을 한다.
-- DOM에 추가적인 노드를 삽입하지 않으면서, 그룹화하는 역할은 같음
-- React.fragment(<></>)
는 추가적인 DOM 요소를 줄이기 위해 사용되지만 document.createDocumentFragment()
는 DOM 조작을 효율적으로 수행하기 위해 사용되는 차이점도 있음
나머지 페이지 - 설정 페이지 코드
export const settings = () => {
document.title = "설정";
const getHTML = async () => {
const main = document.createElement("main");
const title = document.createElement("h1");
title.textContent = "설정";
main.appendChild(title);
const description = document.createElement("p");
description.textContent =
"Lorem ipsum dolor sit amet, consectetur adipisicing elit. At dolores eius ipsa labore laborum magni non, quae recusandae reprehenderit repudiandae sapiente similique tempore ullam veniam veritatis? Aut harum iste quia";
main.appendChild(description);
const homeLinkWrapper = document.createElement("p");
const homeLink = document.createElement("a");
homeLink.setAttribute("href", "/");
homeLink.setAttribute("data-link", "");
homeLink.textContent = "홈으로";
homeLinkWrapper.appendChild(homeLink);
main.appendChild(homeLinkWrapper);
return main;
};
return {
getHTML,
};
};
export const settings = () => {
document.title = "설정";
const getHTML = async () => {
const main = document.createElement("main");
const title = document.createElement("h1");
title.textContent = "설정";
main.appendChild(title);
const description = document.createElement("p");
description.textContent =
"Lorem ipsum dolor sit amet, consectetur adipisicing elit. At dolores eius ipsa labore laborum magni non, quae recusandae reprehenderit repudiandae sapiente similique tempore ullam veniam veritatis? Aut harum iste quia";
main.appendChild(description);
const homeLinkWrapper = document.createElement("p");
const homeLink = document.createElement("a");
homeLink.setAttribute("href", "/");
homeLink.setAttribute("data-link", "");
homeLink.textContent = "홈으로";
homeLinkWrapper.appendChild(homeLink);
main.appendChild(homeLinkWrapper);
return main;
};
return {
getHTML,
};
};
나머지 페이지 - 포스트 페이지 코드
export const posts = () => {
document.title = "포스트";
const getHTML = async () => {
const main = document.createElement("main");
const title = document.createElement("h1");
title.textContent = "포스트";
main.appendChild(title);
return main;
};
return {
getHTML,
};
};
export const posts = () => {
document.title = "포스트";
const getHTML = async () => {
const main = document.createElement("main");
const title = document.createElement("h1");
title.textContent = "포스트";
main.appendChild(title);
return main;
};
return {
getHTML,
};
};
#공통로직 분리
위 view 코드들은 title을 수정하고, getHTML
을 정의해서 리턴하는 동일한 구조를 가지고 있다. 공통 로직을 분리해서 createView
함수를 작성해보자.
#createView.js
export const createView =
(title, content) =>
() => {
document.title = title;
const getHTML = async () => content()
return {
getHTML,
};
};
export const createView =
(title, content) =>
() => {
document.title = title;
const getHTML = async () => content()
return {
getHTML,
};
};
#home.js
에서 createView
를 사용하도록 수정
import { createView } from "./createView.js";
const createHomeContent = () => {
const fragment = document.createElement("main");
const title = document.createElement("h1");
title.textContent = "홈";
fragment.appendChild(title);
const subTitle = document.createElement("h2");
subTitle.textContent = "바닐라 JS로 SPA 만들기";
fragment.appendChild(subTitle);
const description = document.createElement("p");
description.textContent = "Lorem ipsum ... Aut harum iste quia";
fragment.appendChild(description);
const links = document.createElement("ul");
const postsLink = document.createElement("li");
const postsLinkAnchor = document.createElement("a");
postsLinkAnchor.setAttribute("href", "/posts");
postsLinkAnchor.setAttribute("data-link", "");
postsLinkAnchor.textContent = "최근 게시물 보기";
postsLink.appendChild(postsLinkAnchor);
links.appendChild(postsLink);
const settingsLink = document.createElement("li");
const settingsLinkAnchor = document.createElement("a");
settingsLinkAnchor.setAttribute("href", "/settings");
settingsLinkAnchor.setAttribute("data-link", "");
settingsLinkAnchor.textContent = "설정 페이지";
settingsLink.appendChild(settingsLinkAnchor);
links.appendChild(settingsLink);
fragment.appendChild(links);
return fragment;
};
export const home = createView("바닐라 SPA", createHomeContent);
import { createView } from "./createView.js";
const createHomeContent = () => {
const fragment = document.createElement("main");
const title = document.createElement("h1");
title.textContent = "홈";
fragment.appendChild(title);
const subTitle = document.createElement("h2");
subTitle.textContent = "바닐라 JS로 SPA 만들기";
fragment.appendChild(subTitle);
const description = document.createElement("p");
description.textContent = "Lorem ipsum ... Aut harum iste quia";
fragment.appendChild(description);
const links = document.createElement("ul");
const postsLink = document.createElement("li");
const postsLinkAnchor = document.createElement("a");
postsLinkAnchor.setAttribute("href", "/posts");
postsLinkAnchor.setAttribute("data-link", "");
postsLinkAnchor.textContent = "최근 게시물 보기";
postsLink.appendChild(postsLinkAnchor);
links.appendChild(postsLink);
const settingsLink = document.createElement("li");
const settingsLinkAnchor = document.createElement("a");
settingsLinkAnchor.setAttribute("href", "/settings");
settingsLinkAnchor.setAttribute("data-link", "");
settingsLinkAnchor.textContent = "설정 페이지";
settingsLink.appendChild(settingsLinkAnchor);
links.appendChild(settingsLink);
fragment.appendChild(links);
return fragment;
};
export const home = createView("바닐라 SPA", createHomeContent);
다른 페이지들도 위처럼 처리해주면 된다.
#마치며
이번 포스팅에서는 뷰 로직의 추상화와 그것을 활용한 화면 구성 방법에 대해서 살펴보았다.
바닐라JS로 SPA를 만들다보면, JSX가 너무 그리워지는 것 같다. 프레임워크의 편리함과 컴포넌트 기반의 구조가 얼마나 강력한지, 직접 SPA를 구축하면서 체감하게됐다.
다음 포스팅에서는 더 다양하고 복잡한 라우팅 문제를 해결하기 위해 posts/:id
형식의 동적 라우팅 처리 방법과 index.html
에 있는 네비게이션을 레이아웃으로 추출하는 방법에 대해서 알아보자.