๐Ÿš€ ์ฑŒ๋ฆฐ์ง€ ์‹œ์ž‘

ํ”„๋ก ํŠธ์—”๋“œ๋ฅผ ๊ณต๋ถ€ํ•˜๋ฉฐ,ํ•˜๋ฉด ํ• ์ˆ˜๋ก ๊ฒฐ๊ตญ ๊ธฐ๋ฐ˜์ธ Javascript๊ฐ€ ์ค‘์š”ํ•˜๋‹ค๋Š” ์ƒ๊ฐ์ด ๋“ค์—ˆ๋‹ค.๊ทธ๋ž˜์„œ ํ”„๋ ˆ์ž„ ์›Œํฌ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ ๋„ ๊ฐœ๋ฐœํ•  ์ˆ˜ ์žˆ๋Š” ๋Šฅ๋ ฅ์„ ์˜ฌ๋ฆฌ๊ณ  ์‹ถ์–ด ์ฐธ์—ฌํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค.

๋ฐฐํฌ ๋งํฌ : HPNY-2023

์ฃผ์š” ์ฝ”๋“œ ์„ค๋ช…

SPA ๊ตฌํ˜„

router.js

const ROUTE_CHANGE_EVENT = 'ROUTE_CHANGE';

export const init = (onRouteChange) => {
  window.addEventListener(ROUTE_CHANGE_EVENT, () => {
    onRouteChange();
  });
};

export const routeChange = (url, params) => {
  history.pushState(null, null, url);
  window.dispatchEvent(new CustomEvent(ROUTE_CHANGE_EVENT, params));
};

SPA ๋ผ์šฐํŒ…์„ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด์„œ ์ปค์Šคํ…€ ์ด๋ฒคํŠธ๋ฅผ ํ†ตํ•ด์„œ History๋ณ€๊ฒฝ์„ ๊ฐ์ง€ํ•˜๊ณ ,
ํŽ˜์ด์ง€ ์ด๋™์ด ์•„๋‹Œ ์›ํ•˜๋Š” ๋™์ž‘(onRouteChange)์„ ํ• ์ˆ˜ ์žˆ๋„๋ก ํ•˜์˜€๋‹ค.

App.js

class App {
  $target;

  constructor({ $target }) {
    this.$target = $target;
    init(this.route.bind(this)); // history ๋ณ€๊ฒฝ ๊ฐ์ง€, 
    window.addEventListener('popstate', this.route.bind(this));
    this.route();
  }

  route() {
    const { pathname } = location;

    this.$target.innerHTML = ` 
      <header class='header'></header>
      <div class='content'></div>
    `;

    const $content = this.$target.querySelector('.content');
    const $header = this.$target.querySelector('.header');

    new Header($header);

    if (pathname === '/') {
      new Home($content, { title: 'Home' });
    } else if (pathname.indexOf('post') === 1) {
      const [, , postId] = pathname.split('/');
      new DetailPage($content, { postId, title: 'Detail' });
    } else if (pathname.indexOf('edit') === 1) {
      const [, , postId] = pathname.split('/');
      new Edit($content, { postId, title: 'Edit' });
    } else if (pathname === '/write') {
      new WritingPage($content, { title: 'Write' });
    }
  }
}

init()ํ•จ์ˆ˜์˜ parameter๋กœ routeํ•จ์ˆ˜๋ฅผ ์ „๋‹ฌํ•ด,
history ๋ณ€๊ฒฝ ์‹œ์— ํŽ˜์ด์ง€ ์ด๋™์ด ์•„๋‹Œ route ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜์˜€๋‹ค.

route ํ•จ์ˆ˜์—์„œ๋Š” path์— ๋”ฐ๋ผ ํ•ด๋‹นํ•˜๋Š” Component๋ฅผ ํŽ˜์ด์ง€์— ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ๊ฒŒ ํ•œ๋‹ค.

๋˜ํ•œ, ๋’ค๋กœ๊ฐ€๊ธฐ๋ฅผ ๋ˆ„๋ฅธ๊ฒฝ์šฐ์—๋Š” popstate์ด๋ฒคํŠธ๋ฅผ ๋ฐœ์ƒ์ด ๋˜๋Š”๋ฐ, popstate ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด routeํ•จ์ˆ˜๋ฅผ ์‹คํ–‰์‹œ์ผœ, SPA ๋’ค๋กœ๊ฐ€๊ธฐ ๋™์ž‘์„ ๊ตฌํ˜„ํ•˜์˜€๋‹ค.

Component ๊ธฐ๋ฐ˜ ๊ตฌ์กฐ

Component.js

import { routeChange } from '@/router';

class Component {
  state;
  props;
  $target;

  constructor($target, props) {
    this.$target = $target;
    this.props = props;
    this.init();
    this.render();
  }

  init() {} // ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ƒ์„ฑ๋˜์—ˆ์„๋•Œ, ์ฒ˜์Œ ํ•œ๋ฒˆ๋งŒ ์‹คํ–‰

  setState(nextState) {
    this.state = { ...this.state, ...nextState };
    this.render();
  }

  view() { // Component์˜ dom ๊ตฌ์กฐ 
    return ``;
  }

  mount() {} // dom์ด ์ถ”๊ฐ€๋œ ์ดํ›„์— ์‹คํ–‰๋˜์–ด์•ผ ํ•  ๋™์ž‘๋“ค

  render() {
    this.$target.innerHTML = this.view();
    this.mount();
  }

  navigate(url, params) { // SPA ๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•œ ํŽ˜์ด์ง€ ์ด๋™ ํ•จ์ˆ˜
    routeChange(url, params);
  }

  querySelectorChild(selector) {
    return this.$target.querySelector(selector);
  }
}

export default Component;

๊ณตํ†ต์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š” ์ฝ”๋“œ๋“ค์„ class ๋ฌธ๋ฒ•์œผ๋กœ ์ถ”์ƒํ™” ์‹œ์ผœ, ํ•„์š”ํ•œ ๋ถ€๋ถ„์—์„œ๋Š” ์ƒ์†์„ ๋ฐ›์•„ ๋ณ€๊ฒฝํ• ์ˆ˜ ์žˆ๋„๋ก ๊ตฌํ˜„ํ•˜์˜€๋‹ค.

  • setState : state๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ render๋ฅผ ์‹คํ–‰
    1. state๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด render์„ ์‹คํ–‰ํ•œ๋‹ค.
    2. state๋Š” setState๋กœ๋งŒ ๋ณ€๊ฒฝ์„ ํ•ด์•ผํ•œ๋‹ค.
      ์œ„์˜ ๋‘ ๊ทœ์น™์„ ์ง€์ผœ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ• ์ˆ˜ ์žˆ๋„๋ก ํ•˜์˜€๋‹ค.

๋ฐ‘์˜ Comment.js ์ฝ”๋“œ๋Š” Component๋ฅผ ์‚ฌ์šฉํ•œ ์˜ˆ์‹œ์ด๋‹ค.

Comment.js


class Comment extends Component {
  view() {
    const { content } = this.props.comment;
    return `
      <div class="comment__content">${content}</div>
      <button class="comment__delete-btn"></button>
    `;
  }

  mount() {
    this.querySelectorChild(`.comment__delete-btn`).addEventListener(
      'click',
      this.handleCommentDelete.bind(this),
    );
  }

  async handleCommentDelete() {
    const { comment, refetch } = this.props;
    await deleteComment(comment.commentId);
  }
}

view ํ•จ์ˆ˜์— Comment Component์˜ DOM ๊ตฌ์กฐ๋ฅผ ์ž‘์„ฑํ•ด ๋ฆฌํ„ดํ•˜๊ณ ,
mount ํ•จ์ˆ˜์—์„œ ๋ฒ„ํŠผ์— ๋Œ“๊ธ€์„ ์‚ญ์ œํ•˜๋Š” ์ด๋ฒคํŠธ๋ฅผ ์ถ”๊ฐ€ํ•˜์˜€๋‹ค.

๋Œ“๊ธ€ ๋‚ด์šฉ (content)๋Š” ์ปดํฌ๋„ŒํŠธ์˜ props๋กœ ๋ฐ›์•„, class ๋‚ด๋ถ€์—์„œ ์‚ฌ์šฉ ํ•  ์ˆ˜ ์žˆ๋„๋กํ•˜์˜€๋‹ค.

API

api.js

export const requestPOST = async (url, body) => {
  try {
    const response = await fetch(`${BASE_PATH}${url}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(body),
    });
    const json = await response.json();
    if (response.ok) {
      return json;
    }
    if (json.code === 400) {
      throw new Error(json.message);
    }
    throw new Error('API ํ†ต์‹  ์‹คํŒจ');
  } catch (error) {
    throw error;
  }
};

api์˜ ๊ฒฝ์šฐ์—๋Š” GET, POST, DELETE, PATCH๋งˆ๋‹ค api ํ…œํ”Œ๋ฆฟ์„ ๋งŒ๋“ค์–ด, ํ•ด๋‹น ๋ฉ”์†Œ๋“œ์— url, body๋ฅผ parameter์„ ๋„˜๊ฒจ์„œ ์‚ฌ์šฉํ•˜์˜€๋‹ค.

๋งŒ์•ฝ error๊ฐ€ ๋ฐœ์ƒํ•œ ๊ฒฝ์šฐ, throw error์„ ์ด์šฉํ•˜์—ฌ ํ•ด๋‹น api๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ฝ”๋“œ์—์„œ ์ปค์Šคํ…€ ํ•˜์—ฌ ์—๋Ÿฌ ํ•ธ๋“ค๋ง์„ ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜์˜€๋‹ค.

์‚ฌ์šฉํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

vite

express๋ฅผ ์ด์šฉํ•ด์„œ ์„œ๋ฒ„๋ฅผ ๊ตฌํ˜„ํ•˜๊ธฐ๋Š” ๋ฒˆ๊ฑฐ๋กœ์› ๊ณ ,
vite์˜ build์†๋„๊ฐ€ ๋งค์šฐ ๋นจ๋ผ ์ข‹๋‹ค๊ณ  ์•Œ๊ณ ์žˆ์–ด์„œ ๋ฒˆ๋“ค๋Ÿฌ๋กœ vite๋ฅผ ์„ ํƒํ•˜์˜€๋‹ค.


ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋ฉด์„œ ์–ด๋ ค์› ๋˜ ์  / ๊ณ ๋ฏผํ–ˆ๋˜ ์ 

"์ปดํฌ๋„ŒํŠธ ๊ธฐ๋ฐ˜ ๊ฐœ๋ฐœ"

vallina js๋ฅผ ์ด์šฉํ•˜์—ฌ React์ฒ˜๋Ÿผ ์ปดํฌ๋„ŒํŠธ ๊ธฐ๋ฐ˜ ๊ตฌ์กฐ๋กœ ์ž‘์„ฑํ•˜๋ ค๊ณ  ํ•˜์˜€๋‹ค.
๋น„์Šทํ•˜๊ฒŒ ๊ตฌํ˜„ํ•˜๋ ค๊ณ  ํ•˜๋‹ค๋ณด๋‹ˆ ์ƒ๊ฐํ–ˆ๋˜๊ฒƒ ๋ณด๋‹ค ์‰ฝ์ง€ ์•Š์•˜๊ณ , ๋ Œ๋”๋ง ์ด์Šˆ ๋“ฑ์ด ์ƒ๊ฒผ๋Š”๋ฐ, ๊ฒฐ๊ตญ ์ด ๋ฌธ์ œ๋Š” ๊ฐ€์ƒ๋”์„ ์“ฐ์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์–ด์ฉ”์ˆ˜ ์—†์—ˆ๋˜๊ฒƒ ๊ฐ™๋‹ค.

๊ทธ๋ž˜์„œ React์™€๋Š” ์กฐ๊ธˆ ๋‹ค๋ฅด๊ฒŒ ๊ธฐ๋ฐ˜ dom์„ ์ƒ์„ฑํ•˜๊ณ , ๊ทธ ์œ„์น˜์— ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ƒ์„ฑํ•ด์ฃผ๋Š” ๋ฐฉ๋ฒ•์„ ์ด์šฉํ•˜์˜€๋‹ค. (์ƒ์„ฑํ•˜๊ณ  ์‹ถ์€ ์œ„์น˜์— dom์„ ๋ฏธ๋ฆฌ ์ƒ์„ฑํ•ด์•ผํ•œ๋‹ค๋Š” ๋‹จ์ ์ด ์žˆ์ง€๋งŒ,)

"es6 class ๋ฌธ๋ฒ•"

์ฒ˜์Œ์— function ํ•จ์ˆ˜๋ฅผ ์ด์šฉํ•˜์—ฌ์„œ ๊ตฌํ˜„์„ ํ•˜์˜€๋‹ค. ๊ทธ๋Ÿฐ๋ฐ setState์™€ ๊ฐ™์€ ๋กœ์ง๋“ค์ด ๊ณ„์†ํ•ด์„œ ์ค‘๋ณต๋˜๋Š” ๋ฌธ์ œ๊ฐ€ ์ƒ๊ฒผ๋‹ค.

๊ทธ๋ž˜์„œ ์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด es6 class๋ฌธ๋ฒ•์„ ์‚ฌ์šฉํ•œ ์ƒ์†์„ ์ด์šฉํ•˜๊ธฐ๋กœ ํ•˜์˜€๋‹ค.
๊ณตํ†ต์ ์ธ ํ•จ์ˆ˜๋“ค์„ ๊ธฐ๋ฐ˜ ์ปดํฌ๋„ŒํŠธ์— ๊ตฌํ˜„ํ•ด๋‘๊ณ , ํ•„์š”ํ•˜๋‹ค๋ฉด ํ•จ์ˆ˜๋“ค์„ overrideํ•˜๊ฑฐ๋‚˜ ์ƒˆ๋กœ ๋งŒ๋“ค์–ด์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜์˜€๋‹ค.

function (์ „)class (ํ›„)

"innerHTML vs appendChild"

js์˜ innerHTML์˜ ๊ฒฝ์šฐ, cross site scripting ๊ณต๊ฒฉ์— ์ทจ์•ฝํ•˜๋‹ค๋Š” ๋ณด์•ˆ์ƒ์˜ ๋ฌธ์ œ๊ฐ€ ์žˆ์–ด, ์ปดํฌ๋„ŒํŠธ ๊ธฐ๋ฐ˜ ๊ตฌ์กฐ๋กœ ๊ตฌํ˜„์„ ํ•˜๋ฉด์„œ ๊ฐ€๋Šฅํ•œ appendChild๋ฅผ ์ด์šฉํ•˜์—ฌ ๊ตฌํ˜„์„ ํ•˜๊ณ  ์‹ถ์—ˆ๋‹ค.

ํ•˜์ง€๋งŒ ๊ทธ๋ ‡๊ฒŒ ๋˜๋ฉด ๋ฆฌ๋ Œ๋”๋ง์ด ๋˜์ง€ ์•Š๊ฑฐ๋‚˜, ๋ Œ๋”๋ง ๋  ๋•Œ ๋งˆ๋‹ค ๋™์ผํ•œ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ถ”๊ฐ€๋˜๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค.
๊ทธ๋ž˜์„œ ์–ด์ฉ”์ˆ˜ ์—†์ด innerHTML์„ ์ด์šฉํ•˜์—ฌ ๊ธฐ๋ฐ˜ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ตฌํ˜„ํ• ์ˆ˜ ๋ฐ–์— ์—†์—ˆ๋‹ค.

๊ตฌํ˜„ํ•œ render() - Component.js

๋ฐฐํฌ ๋งํฌ

HPNY-2023

profile
์•ˆ๋…•ํ•˜์„ธ์š” ๐Ÿ˜š

1๊ฐœ์˜ ๋Œ“๊ธ€

comment-user-thumbnail
2024๋…„ 1์›” 26์ผ

๐Ÿ‘

๋‹ต๊ธ€ ๋‹ฌ๊ธฐ