반응형

리액트 라우터 나 라이브러리를 사용하지 않고 순수 자바스크립트만으로 라우터를 구현해서 Single Page Application을 구현하는 문제를 학원에서 풀어보았다. 이런 걸 해볼 때마다 라이브러리 만든 사람방향으로 절을 올리고 문제를 만든 사람 방향으로는 두 번 절을 올리고 싶은 심정이다. 한번 풀어보쟈!

 

코드샌드박스로 완성본을 올려놓고싶었는데 같은 코드가 코드샌드박스에선 실행이 안된다.. 어째서...ㅜㅜ

그래서 캡쳐로 대신하겠다.

 

index.js

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="./style.css">
</head>
<body>
    <nav class="navbar">
        <a href="/">HOME</a>
        <a href="/post/123">POST</a>
        <a href="/shop">SHOP</a>
    </nav>
    <div id="app"></div>
    <script src="./main.js" type="module"></script>
</body>
</html>

각 페이지로 넘어갈 버튼들의 마크업이다 자바스크립트를 연결해 주는데 데브매칭 문제들은 대부분 컴포넌트로 구현하기 때문에 type을 module로 하고 나머지 요소들을 자바스크립트에서 컴포넌트로 만들어서 넣어준다.

 

style.css

#app{
    text-align: center;
    color: #2c3e50;
    margin-top:60;
}

.navbar{
    margin-top:60px;
    text-align: center;
}

.navbar > a{
    display: inline-block;
    font-size: 32px;
    text-decoration: none;
    border-radius: 18px;
    background-color: #8040ff;
    color: white;
    padding: 5px 10px;
} 
.navbar > a:hover{
    transform: scale(0.95);
}

// 라우팅되었을때 렌더링될 컴포넌트의 스타일
.mainPage{
    background-color: #4a4b43;
    padding:50px 0;
    color:white
}
.postPage{
    background-color: #40d2ff;
    padding:50px 0;
    color:white
}
.shopPage{
    background-color: #ff40cf;
    padding:50px 0;
    color:white
}

css는 딱히 설명할 게 없다.. 아직 화면에 렌더링 되지 않은 컴포넌트의 스타일도 미리정의해 놨다는 거 정도?

 

html,css 까지 끝난상태

여기까지 하면 이렇게 예쁜 버튼 3개(버튼태그는 아니지만)가 만들어진 걸 볼 수 있다.

 

이제 자바스크립트로 들어가 보자.

우선 컴포넌트들과 필요한 모듈들을 만들 src폴더를 생성해 준다 모든 모듈은 src폴더 내에서 관리해 준다.

 

src / app.js

// import Router from "./router.js"
// import { navigate } from "./utils/navigate.js"

export default function App({target}){
    this.container = target
    const init=()=>{
        document.querySelector(".navbar").addEventListener('click',(e)=>{
            e.preventDefault()
            const targetURL = e.target.href.replace("http://127.0.0.1:5500","")
            console.log(targetURL);
            // navigate(targetURL)
        })
        // new Router(target)
    }
    init()
}

주석처리 한 부분은 아직 구현하지 않은 부분이다 이따가 차례대로 구현해서 넣어주자.

App컴포넌트는 target을 인수로 받는다 target 은 App컴포넌트에서 렌더링 해줄 요소들이 들어갈 위치이다.

target = document.querySelector("#app")

init메서드를 만들어준다

init메서드는. navbar

    <nav class="navbar">
        <a href="/">HOME</a>
        <a href="/post/123">POST</a>
        <a href="/shop">SHOP</a>
    </nav>

이 부분에 이벤트를 클릭이벤트를 생성하고 클릭했을 때 a태그의 페이지가 이동되는 이벤트를 막아주고 preventDefault()

클릭한 a태그의 href 주소를 받아서 앞의 로컬주소는 공백으로 처리해서  a태그에 달아놓은 주소만 받는다.

"http://127.0.0.1:5500/shop"  ==> "/shop"으로 변환시켜 준다.

init() 만들어놓은 메서드를 실행. App컴포넌트가 불려서 실행되면 실행된다.

 

main.js

import App from "./src/app.js";

window.addEventListener("DOMContentLoaded",()=>{
    new App({target:document.querySelector("#app")})
})

만들어놓은 App모듈을 연결시켜 준다 html과 연결된 유일한 자바스크립트는 main.js하나이고 

main.js에서 App모듈을 불러서 렌더링 해주고 App모듈에 여러 모듈들을 불러서 렌더링 해준다

 

index.html -- main.js -- app.js -- 모듈 1

                                                --모듈 2

                                                --모듈 3

 

이런 식으로 연결해서 사용해 준다 , 리액트와 비슷한 것 같기도 아닌 것 같기도..

 

이제 app에서 필요한 요소들을 하나씩 만들어주자.

a태그를 클릭했을 때. 이벤트를 막고 href를 받았다. 이 href를 받아서 넘겨주고 주소창에 추가해줘야 한다.

 

href를 받을 커스텀이벤트 함수를 만들어주자

src / utils / navigate.js

export const navigate = (to)=>{
// const 이벤트개체 = new CustomEvent("이벤트이름",{detail:{키:밸류}})
    const historyChangeEvent = new CustomEvent("historyChange",{
        // to 이동하게 될 url
        detail:{to}
    })
    dispatchEvent(historyChangeEvent);
}

 나도 커스텀이벤트함수는 처음 본 거라 좀 생소한데 navigate함수를 실행하면 to라는 파라미터를 받고 

CustomEvent를 만들 때 detail에 to키에 넣어준다. 

만들어진 이벤트개체를 dispatchEvent 해주면 이벤트를 호출한다. 

 

navigate함수를 실행하면 이벤트함수를 만들고 이벤트함수를 호출한다. 

여기서는 호출되었을 때의 동작은 없기때문에 이따가 다른 모듈에서 호출되었을때의 동작을 만들어 준다.

 

렌더링 할 페이지들을 먼저 만들어주겠다.

src / pages / Main.js

export default function Main(target){
    console.log(target)
    this.container = target
    this.setState = ()=>{
        this.render();
    }
    this.render = () =>{
        this.container.innerHTML = `
        <div class = "mainPage">
        메인 페이지에요.
        </div>
        `;
    }
    this.render()
}

src / pages / Pages.js

export default function Shop(target){
    this.container = target
    this.setState = ()=>{
        this.render();
    }
    this.render = () =>{
        this.container.innerHTML = `
        <div class = "shopPage">
        SHOP페이지에요.
        </div>
        `;
    }
    this.render()
}

src / pages / Post.js

export default function Post(target){
    this.container = target
    this.setState = ()=>{
        this.render();
    }
    this.render = () =>{
        this.container.innerHTML = `
        <div class = "postPage">
        POST페이지에요.
        </div>
        `;
    }
    this.render()
}

target을 전달받아서 target에 html요소를 렌더링 해주는 모듈들이다. this.setState메서드는 지금은 없어도 상관없긴 한데 나중에 값을 전달받거나 할 때 사용할 수도 있어서 만들어두었다.

 

이제 app에서 href를 받아서 유효성을 검사하고 해당하는 href에 page를 보낼 수 있는 모듈을 만들어보자

src / constant / routerinfo.js

import Main from "../pages/Main.js";
import Shop from "../pages/Pages.js";
import Post from "../pages/Post.js";

export const routes = [
    {path:/^\/$/, element:Main}, // "/"
    {path:/^\/post\/[\w]+$/, element:Post},  // "/post/123 , 456"
    {path:/^\/shop$/, element:Shop}, // " /shop "
]

배열의 path에는 정규표현식이 작성되어 있어서 path로 유효성을 검사하고 알맞다면 해당 객체를 받을 수 있는 배열메서드를 사용해서 element에 있는 모듈을 받아올 수 있다.

 

이제 모듈을 받아서 렌더링 해주고 href주소를 받아서 주소창에 입력해 줄 모듈 router를 만들어보자.

src / router

import { routes } from "./constants/routerinfo.js";

export default function Router(target){
    this.container = target;
    const findMatchedRoute = () =>{
        //pathname 이 정규표현식에 true로 나오는 객체를리턴
        return routes.find(route => route.path.test(location.pathname)); //test 메서드는 정규표현식에 값을넣어서 true false 로 반환함
    }
    
    const route = () =>{
        const TargetPage = findMatchedRoute().element;
        new TargetPage(target)
    }
    const init = () =>{
        window.addEventListener("historyChange",({detail})=>{ //커스텀이벤트에서 전달해주는 detail을 객체구조분해할당으로 받음
            const {to} = detail;
            history.pushState(null,"",to); //페이지이동없이 주소를바꿔주는 메소드 (넘겨줄데이터,타이틀,변경할주소)
            route() //pushState로 주소를 변경해주고 라우트함수를 실행시킴
        })
        window.addEventListener("popstate",()=>{
        //뒤로가기버튼을 클릭했을때 실행되는 이벤트
        //주소가 이전주소로바뀌면서 route를 실행하면 이전주소에 해당하는 컴포넌트렌더링
            route()
        })
    }
    init();
    route();
}

router는 target을 전달받아서 렌더링 할 요소에 전달해 준다.

init( ) 메서드는 아까 dispatch 한 커스텀이벤트의 동작을 만들어준다 historyChange이벤트가 발생했을 때 이벤트에서 전달해 주는 detail을 파라미터로 함수 안에서 사용하고 detail안에 있는 to를 구조분해 할당해서 사용한다

to  에는 "/" , "/shop" , "/post/123" 같은 클릭한 a태그의 href 값이 들어있다.

history.pushState는 주소창을 변경시켜 줄 수 있는 메서드이다 pushState(넘겨주는 데이터, 타이틀, 변경할 주소)가 파라미터로 들어간다 여기에 to를 넣으면 현재주소에서 /shop 이 추가된 주소로 바뀐다 (페이지변경 없이!!!)

주소창의 주소를 변경시켜 주고

route메서드를 실행시키는데 

route메서드는 findMatchedRoute메서드를 실행시키고 값을 리턴 받은 후 리턴 받은 값의 element요소를 TargetPage에 할당한다

findMatchedRoute는 아까 만든 정규표현식 배열을 이용하는 메서드인데 정규표현식배열에 find메서드를 이용해서 true값을 리턴하는 배열요소를 받아온다. 현재주소(location.pathname)를 배열에

정규표현식이 들어있는 path키에 test메서드(정규표현식을 만족하는지 판별하는 메서드 맞으면  true 아니면 false 리턴)

안에 넣어서 정규표현식을 만족하는지 검사한 후 리턴해준다.

 

 

이제 99.99% 완성이다 아까 App에서 주석으로 처리해 놨던 구문들을 풀어주면 끝난다

src /app.js

import Router from "./router.js"
import { navigate } from "./utils/navigate.js"

export default function App({target}){
    this.container = target
    const init=()=>{
        document.querySelector(".navbar").addEventListener('click',(e)=>{
            e.preventDefault()
            const targetURL = e.target.href.replace("http://127.0.0.1:5500","")
            console.log(targetURL);
            navigate(targetURL)
        })
        new Router(target)
    }
    init()
}

 

 init 메서드를 실행하면 

a태그의 이벤트를 막고 navigate에 targetURL을 넣어서 historyChange이벤트를 만들고 디스패치해 주고

Router에 target을 넣어서 실행시켜 준다 

그러면 Router에서 클릭한 주소를 historyChange이벤트에서 받아서 historypush.state로 주소에 넣어주고

해당하는 주소를 배열에서 찾아서 렌더링 할 모듈을 받아와서 타깃위치에 렌더링 해준다.

 

완성이미지

012
각 모듈이 렌더링될때의 이미지

 

 

나도 처음 배운 내용이고 이해하기 쉽게 좀 길게 풀어서 설명하다 보니 두서없는 설명이 된 것 같은데 일단 나는 알아볼 수 있으니 복습용으로는 충분하지만 혹시라도 누군가가 보고 이해가 안 가는 게 있다면 댓글로 알려주면 답 해줄 수 있을 것 같다 

아니면 내 깃허브를 보거나..

 

GitHub - cokeholic-kim/spaVanillaJS

Contribute to cokeholic-kim/spaVanillaJS development by creating an account on GitHub.

github.com

 

반응형

+ Recent posts