바닐라javascript로 spa(single page application)개발하기

2023. 3. 3. 17:44프론트엔드

날짜: 2023년 3월 3일
생성일: 2023년 3월 3일 오후 2:51
태그: 프론트 엔드

이 글을 쓰는 이유?

데브 매칭 대비하기 위해 지난해 진행했던, 과제 테스트 해설을 보고 학습한 내용을 정리하려고 합니다. 출제 내역을 보면 SPA(Single Page Application)을 순수 자바 스크립트로 개발하는 문제가 주로 출제 됐습니다.

그렇기 때문에 자바스크립트의 문법을 응용하면서 문제해결을 위해 어떤 문법이 필요한지 알아보겠습니다. 저도 처음부터 알고는 있었지만, “이 문법을 이런식으로 응용해서 사용하면 되는구나” 를 많이 깨달았습니다.

컴포넌트 만들기

SPA는 컴포넌트를 기반으로 개발을 하기 때문에 원래 html노드요소들을 Class를 통해서 동적으로 생성해줘야 한다. 따라서 index.html 한 페이지와 여러개의 js 파일로 이루어질 것이다.

index.html

// index.html
<html>
  <head>
    <title>spa 개발하기</title>
    <link rel="stylesheet" href="./style.css">
  </head>
  <body>
    <main class="App">
      <div class="main"></div>
    </main>
    <script src="./index.js" type="module"></script>
  </body>
</html>

index.js

// index.js
import App from './App.js';

new App(document.querySelector("body"));

app.js

class App {
    constructor($body) {
        this.$body = $body;
        this.render();
    }

    render() {
    }
}
export default App;
  1. index.js 에서 index.html 의 body와 연결하여 app의 인스턴스를 생성한다.
  2. app constructorrender 함수를 이용해서 바로 $body에 렌더링 될 수 있도록 한다.

Class

class App {
    constructor($body) {
        this.$body = $body;
        this.render();
    }

    render() {
    }
}
export default App;
  • 컴포넌트를 구현하기 위해 constructorrender 함수를 멤버 함수로 필수로 가지고 있어야 한다.
  • constructor 에서는 동적으로 필요한 데이터나 타겟을 갖는 부모노드를 가져온다.
  • 이후 다른 파일에서 사용할 수 있도록 export default로 내보낸다.

 

Dom 트리와 컴포넌트

  • 차후에 Dom 트리 처럼 각각 컴포넌트도 클래스로 만들어서 render함수를 이용해 구현한다.

Dom

노드요소들을 Class를 통해서 동적으로 생성하기 위해서 몇가지 함수를 정의한다.

다음과 같은 마크업을 생성하려고 한다.

<body>
    <header>
        <div class="header header_left">
            <span class="menu_name" id="menu_home">HOME</span>
        </div>
        <div class="header header_right">
            <span class="menu_name" id="menu_signup">SIGNUP</span>
        </div>
    </header>
    ...
</body>

컴포넌트 생성

class Header {
  constructor($body) {
    this.$body = $body;
  }

  render() {

  }
}
  • App.js에서 부모노드를 받아서 $body에 연결해준다.

노드 요소 생성

Document.createElement()

HTML 문서에서, document.createElement() 메서드는 지정한 tagName의 HTML 요소를 만들어 반환합니다.

본래의 마크업

  • header 태그 안에 div와 span을 생성해보자.
const leftDiv = document.createElement("div");
const rightDiv = document.createElement("div");

const leftSpan = document.createElement("span");
const rightSpan = document.createElement("span");

노드 속성 연결

Element.setAttribute()

지정된 요소의 속성 값을 설정합니다. 속성이 이미 있으면 값이 업데이트됩니다. 그렇지 않으면 지정된 이름과 값으로 새 속성이 추가됩니다.

  • div 와 span에 각각 클래스를 속성으로 연결해보자
leftDiv.setAttribute("class", "header-left");
rightDiv.setAttribute("class", "header-right");
span.setAttribute("class", "menu_name");
span.setAttribute("id", "menu_name");
  • 이후에 동적으로 클래스가 변경되야 하거나 이벤트로 css의 변경이 필요할때 사용한다.

노드 텍스트 문서 생성

Document.createTextNode()

새로운 텍스트 노드를 만듭니다. 이 방법은 HTML 문자를 이스케이프하는 데 사용할 수 있습니다.

  • span에 들어갈 text를 생성해보자
//text
const homeTxt = document.createTextNode("HOME");
const signTxt = document.createTextNode("SIGN UP");

부모노드와 연결

Node.appendChild()

appendChild()인터페이스 의 메서드는 지정된 부모노드의 자식목록 끝에 노드를 추가합니다. 주어진 자식이 문서의 기존 노드에 대한 참조인 경우 appendChild()현재 위치에서 새 위치로 이동합니다.

divspantext 를 연결해보자

//appendChild
leftDiv.appendChild(span.appendChild(homeTxt));
rightDiv.appendChild(span.appendChild(signTxt));

$bodyheader{left div, right div} 를 연결해보자

const header = document.createElement("header");

header.appendChild(home_menu);
header.appendChild(signup_menu);
this.$body.appendChild(header);

이후에 app.jsRender에서 Header 클래스의 인스턴스를 생성해준다.

app.js

import Header from "./Header";

class App {
  constructor($body) {
    this.$body = $body;
    this.render();
  }

  render() {
    const header = new Header(this.$body);
    header.render();
  }
}

export default App;

header.js

class Header {
  constructor($body) {
    this.$body = $body;
  }

  render() {
    //create

    const leftDiv = document.createElement("div");
    const rightDiv = document.createElement("div");
    const span = document.createElement("span");

    //class
    leftDiv.setAttribute("class", "header-left");
    rightDiv.setAttribute("class", "header-right");
    span.setAttribute("class", "menu_name");
    span.setAttribute("id", "menu_name");

    //text
    const homeTxt = document.createTextNode("HOME");
    const signTxt = document.createTextNode("SIGN UP");

    //appendChild
    leftDiv.appendChild(span.appendChild(homeTxt));
    rightDiv.appendChild(span.appendChild(signTxt));

    const header = document.createElement("header");

    header.appendChild(home_menu);
    header.appendChild(signup_menu);
    this.$body.appendChild(header);
  }

}

export default Header;
  • 너무 구조가 복잡하고 중복된 코드가 있는것 같으면 따로 함수로 빼는걸 추천한다.

addEventListner

EventTarget.addEventListener()

addEventListener()인터페이스의 메서드는 지정된 EventTarget 이벤트가 대상에 전달될 때마다 호출될 함수를 설정합니다.

  • 요구하는 요소에 클릭이나 드래그, 변경, 이벤트가 필요할때 사용한다.
  • 때에때에 맞는 이벤트를 찾고, 행동을 정의해서 등록하고 사용하면 될 것!
  • 제출 이벤트 시에는 새로고침이 일어나지 않도록 event.precentDefault( )를 사용한다.

 

라우팅 하기

  • spa에서는 단일 페이지이지만, 컴포넌트로 페이지를 구성한다. 하지만 페이지간 이동시 새로고침이 일어나지 않는다.
  • 새로고침 없이 path가 변경되어야 하고, 그 path를 감지해서 render를 해야한다.

History

History.pushState()

HTML 문서 에서 history.pushState()메서드는 브라우저의 세션 기록 스택에 항목을 추가합니다.

pushState(state, unused)
pushState(state, unused, url)
  • state : 상태를 저장하는 json 객체
  • unused : 필수로 넣어야 하는 매개변수
  • url : 새 기록 항목의 URL. 새 url 현재 url 과 같아야함

history.pushState()를 이용하면 새로고침 없이 path를 변경할 수 있다. 이를 이벤트 리스너로 연결시켜 보자

// HOME 메뉴 클릭 이벤트
home_menu.addEventListener("click", () => {
    window.history.pushState("", "", "/web/");
});

CustomEvent

  • url이 변경 됐을 때를 상위 컴포넌트에서 감지 하기 위해서 customEvent를 작성합니다.

CustomEvent()

CustomEvent() 생성자는 새로운CustomEvent를 생성합니다.

https://developer.mozilla.org/ko/docs/Web/API/CustomEvent/CustomEvent

CustomEvent(typeArg);
CustomEvent(typeArg, options);

`typeArg` : 이벤트의 이름을 나타내는 문자열입니다.
`options` : **Optional** 다음 속성을 포함하는 객체입니다. 
    - detail : 이 이벤트 내에 포함할, 이벤트의 세부 정보를 나타내는 값입니다. 
    - event :  생성자의 옵션에 지정할 수 있는 모든 속성.
  • “path가 바뀔 시에 경로가 바뀜 → options의 detail 속성으로 path를 전달”을 커스텀 이벤트로 등록한다.
  • custom 이벤트를 생성하고 dispatcher로 발송한다 이 때, 전체 페이지에 발생하는 이벤트니까 document 객체로 발송 한다.
  • document 객체에 eventListener로 이벤트를 수신한다.

header.js

// HOME 메뉴 클릭 이벤트
home_menu.addEventListener("click", () => {
    window.history.pushState("", "", "/web/");
    const urlChange = new CustomEvent("urlchange", {
        detail: { href: "/web/" }
    });
    document.dispatchEvent(urlChange);
});

// SIGNUP 메뉴 클릭 이벤트
signup_menu.addEventListener("click", () => {
    window.history.pushState("", "", "/web/signup");
    const urlChange = new CustomEvent("urlchange", {
        detail: { href: "/web/signup" }
    });
    document.dispatchEvent(urlChange);
});

app.js

document.addEventListener("urlchange", (e) => {
  let pathname = e.detail.href;

  switch(pathname) {
      case "/web/":
          homePage.render();
          break;
      case "/web/signup":
          signupPage.render();
          break;
      default:
  }
});

데이터 불러오기

Fetch 와 async/ await

Fetch API
는 HTTP 파이프라인을 구성하는 요청과 응답 등의 요소를 JavaScript에서 접근하고 조작할 수 있는 인터페이스를 제공합니다.

  • HTTP 통신을 위해 사용하는것이 대부분이지만, 소스파일 내에 json 파일을 가져올 때도 사용한다.
fetch('http://example.com/movies.json')
  .then((response) => response.json())
  .then((data) => console.log(data));
  • fetchAPI는 promise 객체를 리턴하기 때문에 async await 와 함께 사용할 수 있다.

await

연산자는Promise객체를 기다리기 위해 사용됩니다. 연산자는 async function내부에서만 사용할 수 있습니다. await 문은 Promise가 fulfill되거나 reject될 때까지 async함수의 실행을 일시정지하고, Promise가 fulfill되면 async 함수를 일시 정지한 부분부터 실행합니다. 이때 await
문의 반환값은 Promise에서 fulfill된 값이 됩니다.

async function fetchJson() {
  const result = await fetch("/src/data/users");
  return result;
}
  • 이때 fetchAPI에서 오류가 나면 프로그램 종료가 우려되므로 try catch문과 함께 사용하는게 일반적이다.

Try/Catch

try {
    await fetchJson();
}catch(e){
    console.log(e);
    //오류 처리
}

출처