웹 프론트엔드 개발에 대한 이해도를 높이고 기초부터 숙달하기 위하여, 새로운 프로젝트를 Vanilla JS로 진행해 보기로 했다. 페이지 전환 속도를 감소하여 사용자 경험을 향상시키기 위하여, React처럼 SPA(Single Page Application)로 구현했다.
꽤나 어렵고 번거로운 일이었지만, 직접 구현해보며 어떤 식으로 돌아가는지 공부해 볼 수 있었고, 프레임워크나 라이브러리를 쓰며 얼마나 편하게 작업할 수 있었던 것인지 깨달을 수 있었던 유익한 시간이었다.
구현 시 아래 목표들을 달성하기 위해 많은 고민을 하며 진행했다.
- 페이지 이동 시 URL의 경로도 변경될 것
- URL의 경로가 깔끔할 것
- 뒤로 가기가 가능할 것
- 해당 페이지의 CSS가 적용될 것
- URL의 경로를 직접 수정하여도 해당 페이지로 이동될 것
- 이 때 비정상적인 경로가 입력되었을 경우 404 페이지가 뜨도록 할 것
파일 단위로 차근차근 진행해 보도록 하겠다.
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>프로젝트명</title>
<link id="styles" rel="stylesheet" href="" />
</head>
<body>
<main class="App"></main>
<script type="module" src="/src/App.js"></script>
</body>
</html>
vanilla JS로 SPA를 구현하기 앞서서, index.html 파일을 이렇게 작성했다.
line 6에 css 파일의 href를 비워두어, 페이지 이동 시 해당 페이지에 맞는 css 파일로 바꾸도록 아래에서 구현할 예정이다.
App.js
import changeUrl from "./Router.js";
changeUrl(window.location.pathname);
index.html 파일에서 실행하는 App.js 는 window.location.pathname 로 현재 URL 중 경로 부분 (ex: www.example.com/main 이라면 ‘/main’ 부분)을 가져와 아래에서 만들 changeUrl 함수의 인자로 전달한다. URL을 직접 변경하였을 경우 처리를 위한 부분이다.
라우팅 구현하기
페이지(뷰) 정보를 객체 배열로 저장하기
const routes = [
{ path: "/", page: Login, style: "login" },
{ path: "/main", page: Main, style: "main" },
{ path: "/404", page: NotFound, style: "notfound" },
];
각 페이지 별로 어떤 경로로 이동시킬 것인지, 어떤 css 파일을 연결시킬 것인지를 정의한다. 경로가 변경되어야 하는 페이지는 전부 작성하면 된다.
- path : URL 뒤에 붙을 경로를 작성
- page : import 하여 가져오기 한 페이지의 이름 작성
- style : 페이지 이동 시 함께 바뀌어야 하는 css 파일 이름 작성
요청한 경로를 routes 객체 배열에서 찾는 함수를 만들기
function checkUrl(requestedUrl) {
let match = routes.find((route) => {
if (route.path === requestedUrl) return route;
});
if (match === undefined) return match;
}
인자 requestedUrl 값을 routes 의 path 에서 검색하여, 일치하는 경우에는 해당 객체를 리턴하고 불일치하는 경우에는 undefined를 리턴하는 함수를 만든다.
뷰 갈아끼우기
뷰를 갈아 끼우기 위해 changeUrl 함수를 만들 것이다.
export default function changeUrl(requestedUrl, element) {
//화면 초기화
const $app = document.querySelector(".App");
$app.innerHTML = "";
const match = checkUrl(requestedUrl);
if (match === undefined) changeUrl("/404");
const cssPath = `/src/styles/${match.style}.css`;
document.getElementById("styles").setAttribute("href", cssPath);
if (match.page === Main) {
if (getIsLogin() === null) {
changeUrl("/"); //로그인하지 않은 경우 로그인 페이지로 이동
return;
}
}
history.pushState(null, null, match.path);
match.page();
}
window.addEventListener("popstate", () => {
changeUrl(window.location.pathname);
});
코드 부분 별로 설명하자면,
const $app = document.querySelector(".App");
$app.innerHTML = "";
각 페이지에서 App 클래스 하위로 dom 을 생성하고 있어서, 뷰를 갈아 끼우기 전에 App 내의 내용을 초기화시켜준다.
const match = checkUrl(requestedUrl);
if (match === undefined) changeUrl("/404");
위에서 만들었던 checkUrl 함수를 호출하여 URL 경로가 정상적인지, 어떤 페이지로 바꾸어야 하는지 받고, undefined 일 경우 404 페이지로 띄우기 위해 다시 해당 함수를 ‘/404’ 인자를 넣어 호출한다.
const cssPath = `/src/styles/${match.style}.css`;
document.getElementById("styles").setAttribute("href", cssPath);
바꾸어야 하는 페이지에 해당하는 css 파일을 id가 ‘styles’ 인(index.html 파일에서 href를 비워두었던) 부분을 찾아 href를 해당 css 파일의 경로로 바꾸어 준다.
if (match.page === Main) {
if (getIsLogin() === null) {
changeUrl("/"); //로그인하지 않은 경우 로그인 페이지로 이동
return;
}
}
이 부분은 optional 한 부분인데, 로그인이 필수인 페이지일 경우 여기서 로그인 여부를 걸러줬다.
로그인이 되어 있지 않을 경우 로그인 페이지로 이동된다.
history.pushState(null, null, match.path);
history.pushState 함수는 브라우저 기록 스택에 새로운 히스토리 엔트리를 추가하는 역할을 한다.
이 함수를 호출하면 URL이 내가 변경하고자 하는 URL로 변경되며, 뒤로 가기를 할 수 있도록 브라우저 기록 스택에 저장된다.
pushState(state, title, url) 형식이며
첫 번째 인자로 새로운 세션 기록 항목에 연결할 상태 객체를 전달한다. 저장할 데이터가 없다면 null 을 전달한다.
두 번째 인자로 title 로 지정하고자 하는 문자열을 전달한다.
세 번째 인자로 변경할 경로를 전달한다.
match.page();
// if (element가 필요한 페이지)
// match.page(element);
// else
// match.page();
이제 해당 페이지의 함수를 호출하여 화면을 그려주면 된다!
참고로, changeUrl 함수는 인자로 requestedUrl와 element를 받고 있는데, 인자를 받아야 하는 페이지가 있다면 element 를 페이지 함수의 인자로 넘겨 호출하도록 작성하면 된다.
window.addEventListener("popstate", () => {
changeUrl(window.location.pathname);
});
마지막으로, 이벤트 리스너로 ‘popstate’ 이벤트를 감지한다. ‘popstate’ 이벤트는 브라우저 기록 스택에서 엔트리가 변경될 때마다(ex: 사용자가 뒤로 가기 버튼을 클릭 할 때마다) 감지된다. 이벤트 발생 시 뒤로가기 되게끔 changeUrl(window.location.pathname) 해준다.
이렇게 하면 바닐라 JS로 SPA 구현이 완료되었다!!🎉
아래는 Router.js 의 전체 코드이다.
//Router.js
import Login from "./pages/Login.js";
import Main from "./pages/Main.js";
import Redirect from "./components/Login/Redirect.js";
import NotFound from "./pages/NotFound.js";
import { getUserId, getIsLogin } from "./state/State.js";
const routes = [
{ path: "/", page: Login, style: "login" },
{ path: "/login/oauth2/code", page: Redirect, style: "redirect" },
{ path: "/main", page: Main, style: "main" },
{ path: "/404", page: NotFound, style: "notfound" },
];
function checkUrl(requestedUrl) {
let match = routes.find((route) => {
if (route.path === requestedUrl) return route;
});
if (match === undefined) return match;
}
export default function changeUrl(requestedUrl, element) {
//화면 초기화
const $app = document.querySelector(".App");
$app.innerHTML = "";
const match = checkUrl(requestedUrl);
if (match === undefined) changeUrl("/404");
const cssPath = `/src/styles/${match.style}.css`;
document.getElementById("styles").setAttribute("href", cssPath);
if (match.page !== Redirect) history.pushState(null, null, match.path);
if (match.page === Main) {
if (getIsLogin() === null) {
changeUrl("/"); //로그인하지 않은 경우 로그인 페이지로 이동
return;
}
}
window.addEventListener("popstate", () => {
changeUrl(window.location.pathname);
});
NGINX 환경일 경우,
이 프로젝트는 build 도구를 따로 사용하지 않고, NGINX 에서 index.html을 실행하여 웹페이지를 열도록 구성했다. 이 상황에서 위의 라우팅 구현을 다 했는데도 뷰가 뜨지 않고 NGINX의 404 페이지만 뜨는 문제가 있었다.
이럴 경우 NGINX에서 URL 경로를 판단하여 404 처리가 되도록 설정파일이 작성되어 있을 수 있다. 아래처럼 NGINX 설정파일을 수정해 주면 정상적으로 라우팅 처리가 가능하다.
//nginx/defalut
location / {
root /app/frontend;
try_files $uri $uri/ /index.html;
}
try_files 를 index.html 만 두어, NGINX에서는 무조건 index.html 파일을 실행하게 한다.
(경로 확인은 Router.js 에서 하기 때문에)
출처
'Front-End' 카테고리의 다른 글
손 쉬운 CSS 작성이 가능한, Tailwind CSS 설치&사용하기 (0) | 2024.05.12 |
---|---|
[JS] Expected a JavaScript module script but the server responded with a MIME type of "text/html". 에러 해결하기 (0) | 2024.05.12 |
[Github, 배포] Github Actions으로 환경변수(.env) 관리하기 (0) | 2024.05.12 |
[React] Recoil 새로고침 시 데이터 유지하기 (0) | 2024.05.12 |
[React] Uncaught Error: Invalid hook call. 에러 해결 (0) | 2024.05.12 |