Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[11팀 장원정] [Chapter 1-1] 프레임워크 없이 SPA 만들기 #6

Open
wants to merge 37 commits into
base: main
Choose a base branch
from

Conversation

wonjung-jang
Copy link

@wonjung-jang wonjung-jang commented Dec 15, 2024

12/19일까지 코드 및 PR 업데이트 예정

image

과제 체크포인트

✅ 기본과제

1) 라우팅 구현:

  • History API를 사용하여 SPA 라우터 구현
    • '/' (홈 페이지)
    • '/login' (로그인 페이지)
    • '/profile' (프로필 페이지)
  • 각 라우트에 해당하는 컴포넌트 렌더링 함수 작성
  • 네비게이션 이벤트 처리 (링크 클릭 시 페이지 전환)
  • 주소가 변경되어도 새로고침이 발생하지 않아야 한다.

2) 사용자 관리 기능:

  • LocalStorage를 사용한 간단한 사용자 데이터 관리
    • 사용자 정보 저장 (이름, 간단한 소개)
    • 로그인 상태 관리 (로그인/로그아웃 토글)
  • 로그인 폼 구현
    • 사용자 이름 입력 및 검증
    • 로그인 버튼 클릭 시 LocalStorage에 사용자 정보 저장
  • 로그아웃 기능 구현
    • 로그아웃 버튼 클릭 시 LocalStorage에서 사용자 정보 제거

3) 프로필 페이지 구현:

  • 현재 로그인한 사용자의 정보 표시
    • 사용자 이름
    • 간단한 소개
  • 프로필 수정 기능
    • 사용자 소개 텍스트 수정 가능
    • 수정된 정보 LocalStorage에 저장

4) 컴포넌트 기반 구조 설계:

  • 재사용 가능한 컴포넌트 작성
    • Header 컴포넌트
    • Footer 컴포넌트
  • 페이지별 컴포넌트 작성
    • HomePage 컴포넌트
    • ProfilePage 컴포넌트
    • NotFoundPage 컴포넌트

5) 상태 관리 초기 구현:

  • 간단한 상태 관리 시스템 설계
    • 전역 상태 객체 생성 (예: 현재 로그인한 사용자 정보)
  • 상태 변경 함수 구현
    • 상태 업데이트 시 관련 컴포넌트 리렌더링

6) 이벤트 처리 및 DOM 조작:

  • 사용자 입력 처리 (로그인 폼, 프로필 수정 등)
  • 동적 컨텐츠 렌더링 (사용자 정보 표시, 페이지 전환 등)

7) 라우팅 예외 처리:

  • 잘못된 라우트 접근 시 404 페이지 표시
✅ 심화과제

1) 해시 라우터 구현

  • location.hash를 이용하여 SPA 라우터 구현
    • '/#/' (홈 페이지)
    • '/#/login' (로그인 페이지)
    • '/#/profile' (프로필 페이지)

2) 라우트 가드 구현

  • 로그인 상태에 따른 접근 제어
  • 비로그인 사용자의 특정 페이지 접근 시 로그인 페이지로 리다이렉션

3) 이벤트 위임

  • 이벤트 위임 방식으로 이벤트를 관리하고 있다.

[항해 플러스 프론트엔드 4기] 1주차 과제 회고

항해 플러스 프론트엔드 4기가 시작됐다.
첫 번째 과제는 <프레임워크 없이 SPA 만들기>였다.
총 3주에 걸쳐 진행되는 과제인데 첫 주는 라우팅, 컴포넌트 기반 구조 설계, 전역 상태 관리 구현을 했다.

💰 기술적 성장: 이벤트 위임 활용

항해가 시작되기 전 여러 사전 스터디 자료를 주셨다.
JavaScript 자료를 학습하며 이벤트 위임에 대해서 알게 됐다.
이벤트 위임을 간단하게 설명하면

<nav id="nav">
  <ul>
    <li><a href="/a" class="link">A</a></li>
    <li><a href="/b" class="link">B</a></li>
    <li><a href="/c" class="link">C</a></li>
  </ul>
</nav>

위 코드에서 a 태그에 이벤트 리스너를 추가할 때 기존에는

document.querySelectorAll("a.link").forEach((link) => {
  link.addEventListener("click", (e) => {
    e.preventDefault();
    console.log("click!");
  });
});

document.querySelectorAll로 모든 태그를 선택해 반복문을 통해 일일히 이벤트 리스너를 추가했다.

하지만 이벤트 위임을 사용하면

document.querySelector("#nav").addEventListener("click", (e) => {
  if (e.target.tagName === "A") {
    e.preventDefault();
    console.log("click!");
  }
});

상위 태그에 이벤트 리스너를 한 번만 할당한 뒤 이벤트 발생 target을 검사하여 원하는 로직을 수행할 수 있다.

이벤트 위임은 이벤트 버블링 덕분에 가능하다.
이벤트 버블링은 한 태그에서 이벤트가 발생하면 부모 방향으로 이벤트를 전파하는 현상을 말한다.
즉, 자식 요소에서 발생한 이벤트는 버블링을 통해 부모 요소에서 알 수 있다.
같은 이벤트 리스너를 추가하려는 태그들이 있다면, 공통 부모 태그에 이벤트 리스너 하나만 사용하여 이벤트 처리를 할 수 있다.

💰 과제하며 고민한 부분: 리팩토링

import MainPage from "./pages/MainPage";
import ProfilePage from "./pages/ProfilePage";
import LoginPage from "./pages/LoginPage";
import NotFoundPage from "./pages/NotFoundPage";
import UserStore from "./store/userStore";

const routes = {
  "/": () => MainPage(),
  "/profile": () => ProfilePage(),
  "/login": () => LoginPage(),
  404: () => NotFoundPage(),
};

const hashRoutes = {
  "#/": () => MainPage(),
  "#/profile": () => ProfilePage(),
  "#/login": () => LoginPage(),
  404: () => NotFoundPage(),
};

const router = createRouter();

function createRouter() {
  return function (path) {
    path = path ? path : window.location.pathname;
    let hash = window.location.hash;
    let route = null;
    const user = new UserStore().getUser();

    if (hash === "") {
      if (!user && path === "/profile") path = "/login";
      if (user && path === "/login") path = "/";
      route = routes[path] || routes["404"];
      window.history.pushState(null, "", path);
    } else {
      if (!user && hash === "#/profile") hash = "#/login";
      if (user && hash === "#/login") hash = "#/";
      route = hashRoutes[hash] || hashRoutes[404];
      window.history.pushState(null, "", hash);
    }

    initializeView(route);
  };
}

function initializeView(route) {
  const root = document.getElementById("root");
  render(route, root);
  attachEventListeners(root);
}

function render(route, root) {
  root.innerHTML = route();
}

function attachEventListeners(root) {
  const cloneRoot = root.cloneNode(true);
  cloneRoot.addEventListener("submit", submitEventHandler);
  cloneRoot.addEventListener("click", clickEventHandler);
  root.replaceWith(cloneRoot);
}

function submitEventHandler(e) {
  e.preventDefault();
  const form = e.target;
  const formData = new FormData(form);
  const { id } = form;

  if (id === "login-form") {
    const username = formData.get("username");

    if (username) {
      new UserStore().setUser({ username, email: "", bio: "" });
      router("/profile");
    }
  }

  if (id === "profile-form") {
    const username = formData.get("username");
    const email = formData.get("email");
    const bio = formData.get("bio");

    new UserStore().setUser({ username, email, bio });
    router("/profile");
  }
}

function clickEventHandler(e) {
  const { id, tagName } = e.target;

  if (tagName === "A") {
    e.preventDefault();
    const { href } = e.target;
    let path = href.slice(href.lastIndexOf("/"));
    if (id === "logout") {
      new UserStore().deleteUser();
      path = "/login";
    }
    router(path);
  }
}

window.addEventListener("popstate", () => router());
window.addEventListener("load", () => router());
window.addEventListener("hashchange", () => router());

현재 코드의 문제점은 아래와 같다.

  1. 라우팅, 이벤트 처리, 렌더링이 모두 한 파일에 있다.
  2. 라우터 로직들이 각각 따로 선언되어 있다.

라우터를 분리하기 전에 라우터에게 기대하는 역할은 무엇일까?

개인적으로 생각하는 라우터의 역할은 URL 경로에 따라 알맞는 페이지 컴포넌트를 찾아서 렌더링해주는 것이다.

현재 라우터는 어떤 역할을 수행하고 있는가?

  1. path를 인자로 받거나 없으면 window.location.pathname을 변수에 담는다.
  2. 현재의 hash도 변수에 담는다.
  3. 경로 기반 라우팅인지, 해시 기반 라우팅인지와 로그인 유무에 따라 페이지 컴포넌트를 가져온다.
  4. 브라우저의 history에 경로를 저장한다.
  5. root에 페이지를 렌더링하고 이벤트 리스너를 추가한다.

💵 관심사 분리: 이벤트 리스너 분리

이벤트 리스너를 추가하는 건 라우터의 역할이 아니므로 분리해보자.

function attachEventListeners(root) {
  const cloneRoot = root.cloneNode(true);
  cloneRoot.addEventListener("submit", submitEventHandler);
  cloneRoot.addEventListener("click", clickEventHandler);
  root.replaceWith(cloneRoot);
}

이벤트 위임을 사용해 root에 이벤트 리스너를 추가하고 있다.
root에 내용이 변할 때마다 이벤트 리스너를 새롭게 추가해주고 있는데 생각해보니 그럴 필요가 있나?

  1. root가 변하는게 아니라 자식 요소가 변하기 때문에 이벤트는 한 번만 할당하면 된다.
  2. body 태그에 root 태그만 있으니 코드 가독성을 위해 bodyclick 이벤트와 submit 이벤트 두 개만 추가해주면 router에서 이벤트 리스너를 추가하지 않아도 되고 코드도 줄어든다.
document.body.addEventListener("submit", submitEventHandler);
document.body.addEventListener("click", clickEventHandler);

위 두 줄을 추가하고 attachEventListener 함수와 호출하는 부분의 코드를 삭제했다.

💵 파일 분리: 라우터 로직 캡슐화

이벤트 처리를 분리했으니 라우터 관련 코드를 다른 파일로 분리시켜서 main.js에 필요한 것만 보내도록 변경해보자.

// src/router/createRouter.js
import MainPage from "@/pages/MainPage";
import ProfilePage from "@/pages/ProfilePage";
import LoginPage from "@/pages/LoginPage";
import NotFoundPage from "@/pages/NotFoundPage";
import UserStore from "@/store/userStore";

const routes = {
  "/": () => MainPage(),
  "/profile": () => ProfilePage(),
  "/login": () => LoginPage(),
  404: () => NotFoundPage(),
};

const hashRoutes = {
  "#/": () => MainPage(),
  "#/profile": () => ProfilePage(),
  "#/login": () => LoginPage(),
  404: () => NotFoundPage(),
};

export function createRouter() {
  return function (path) {
    path = path ? path : window.location.pathname;
    let hash = window.location.hash;
    let route = null;
    const user = new UserStore().getUser();

    if (hash === "") {
      if (!user && path === "/profile") path = "/login";
      if (user && path === "/login") path = "/";
      route = routes[path] || routes["404"];
      window.history.pushState(null, "", path);
    } else {
      if (!user && hash === "#/profile") hash = "#/login";
      if (user && hash === "#/login") hash = "#/";
      route = hashRoutes[hash] || hashRoutes[404];
      window.history.pushState(null, "", hash);
    }

    render(route);
  };
}

function render(route) {
  const root = document.getElementById("root");
  root.innerHTML = route();
}

부족한 점이 많지만 그 중 두 가지만 뽑자면

  1. createRouter에서 반환하는 router는 인수로 path를 받아서 사용하기도 하고 인수로 받지 않을 경우에는 window.location.pathname을 받아서 사용한다.
  2. router에서 일반 라우트와 해시 라우트 처리를 모두 하고 있다.

위 두 문제를

  1. path를 받아서 사용하는 navigator 함수를 만들어서 별도의 로직으로 분리.
  2. routerhashRouter 분리.

로 해결해보자.

import MainPage from "@/pages/MainPage";
import ProfilePage from "@/pages/ProfilePage";
import LoginPage from "@/pages/LoginPage";
import NotFoundPage from "@/pages/NotFoundPage";
import UserStore from "@/store/userStore";

export function createRouter() {
  const ROUTES = {
    "/": () => MainPage(),
    "/profile": () => ProfilePage(),
    "/login": () => LoginPage(),
    404: () => NotFoundPage(),
  };

  const HASH_ROUTES = {
    "#/": () => MainPage(),
    "#/profile": () => ProfilePage(),
    "#/login": () => LoginPage(),
    404: () => NotFoundPage(),
  };

  function render(route) {
    const root = document.getElementById("root");
    root.innerHTML = route();
  }

  function validateRouteUser(path) {
    const user = new UserStore().getUser();
    if (!user && path === "/profile") path = "/login";
    if (user && path === "/login") path = "/";
    return path;
  }

  function validateHashRouteUser(hash) {
    const user = new UserStore().getUser();
    if (!user && hash === "#/profile") hash = "#/login";
    if (user && hash === "#/login") hash = "#/";
    return hash;
  }

  return {
    router() {
      let path = window.location.pathname;
      path = validateRouteUser(path);
      const route = ROUTES[path] || ROUTES["404"];
      window.history.pushState(null, "", path);
      render(route);
    },
    hashRouter() {
      let hash = window.location.hash;
      hash = validateHashRouteUser(hash);
      const route = HASH_ROUTES[hash] || HASH_ROUTES[404];
      window.history.pushState(null, "", hash);
      render(route);
    },
    navigator(path) {
      path = validateRouteUser(path);
      const route = ROUTES[path] || ROUTES["404"];
      window.history.pushState(null, "", path);
      render(route);
    },
  };
}

막상 바꾸고 나니, routernavigator 함수는 path를 인수로 받냐, 안 받냐의 차이만 있고 로직이 동일하다.
굳이 분리하는 것보다 합치는 게 좋을 것 같다.

return {
  router(path) {
    path = path || window.location.pathname;
    path = validateRouteUser(path);
    const route = ROUTES[path] || ROUTES["404"];
    window.history.pushState(null, "", path);
    render(route);
  },
  hashRouter(hash) {
    hash = hash || window.location.hash;
    hash = validateHashRouteUser(hash);
    const route = HASH_ROUTES[hash] || HASH_ROUTES[404];
    window.history.pushState(null, "", hash);
    render(route);
  },
};

💵 트러블 슈팅: popstate 발생 시점

window.addEventListener("load", () => router());
window.addEventListener("popstate", () => router());
window.addEventListener("hashchange", () => hashRouter());

createRouter에서 생성한 routerhashRouter는 위와 같이 쓰인다.
직접 서버를 실행했는데 예상과는 다른 결과가 나왔다.
주소창에 #/login 해시 경로를 입력하고 엔터를 눌렀더니 LoginPage가 안 나오고 NotFoundPage가 나왔다.

처음에는 'load 시에 호출하는 router와 겹치는 건가?' 했는데 직접 로그를 찍어보니 hashchange 이벤트 보다 popstate 이벤트가 먼저 발생한다.

따라서 window.location.pathname/로 나오므로 HomePage를 반환하고 주소를 변경한다.
다음에 hashchange가 발생하여 window.location.hash를 찾는데 빈 문자열이므로 NotFoundPage를 반환했다.

슬랙에 남긴 질문 내용

popstate 이벤트는 브라우저에서 뒤로 가기, 앞으로 가기, 새로고침을 했을 때와 history.back(), history.forward(), history.go() 메서드 호출 시에만 발생하는 줄 알았고 찾아봐도 자료를 찾을 수 없어(마이 서칭 쉴력 ㅠㅠ) 항해 슬랙에 질문을 남겼다.

슬랙 답변 내용

다른 팀원분께서 내가 남긴 질문에 댓글을 달아주셨다.👍
간단하게 얘기하면 URL이 바뀌면 popstate가 된다는 내용이다.

그렇다면 코드에 개선이 필요하다.
현재는 createRouter에서 routerhashRouter를 내보내 이벤트에 맞게 할당했다.
하지만 popstate는 해시 변경(주소창에 직접 입력) 시에도 발생하기 때문에 상황에 따라 대처할 라우터 하나만 내보내주는 것이 좋을 것 같다.

function navigator(path) {
  if (window.location.hash) return;
  path = path || window.location.pathname;
  path = validateRouteUser(path);
  const route = ROUTES[path] || ROUTES["404"];
  window.history.pushState(null, "", path);
  render(route);
}

function hashNavigator(hash) {
  hash = hash || window.location.hash;
  hash = validateHashRouteUser(hash);
  const route = HASH_ROUTES[hash] || HASH_ROUTES[404];
  window.history.pushState(null, "", hash);
  render(route);
}

return {
  router() {
    if (window.location.hash) {
      hashNavigator();
    } else {
      navigator();
    }
  },
  navigator,
};

정리하고 나니 처음 코드보다 좋아진 건지 모르겠다...

💵 라우터 로직 다시 한 번 분리

과제 중간 Q&A 시간에 멘토님께서 다른 곳에서 SPA 프로젝트를 한다고 했을 때 갖다 써도 될 정도로 모듈화하는 것을 목표로 해보라는 말씀을 하셨다.

지금까지 정리한 코드를 살펴보자.

import MainPage from "@/pages/MainPage";
import ProfilePage from "@/pages/ProfilePage";
import LoginPage from "@/pages/LoginPage";
import NotFoundPage from "@/pages/NotFoundPage";
import UserStore from "@/store/userStore";

export function createRouter() {
  const ROUTES = {
    "/": () => MainPage(),
    "/profile": () => ProfilePage(),
    "/login": () => LoginPage(),
    404: () => NotFoundPage(),
  };

  const HASH_ROUTES = {
    "#/": () => MainPage(),
    "#/profile": () => ProfilePage(),
    "#/login": () => LoginPage(),
    404: () => NotFoundPage(),
  };

  function render(route) {
    const root = document.getElementById("root");
    root.innerHTML = route();
  }

  function validateRouteUser(path) {
    const user = new UserStore().getUser();
    if (!user && path === "/profile") path = "/login";
    if (user && path === "/login") path = "/";
    return path;
  }

  function validateHashRouteUser(hash) {
    const user = new UserStore().getUser();
    if (!user && hash === "#/profile") hash = "#/login";
    if (user && hash === "#/login") hash = "#/";
    return hash;
  }

  function navigator(path) {
    if (window.location.hash) return;
    path = path || window.location.pathname;
    path = validateRouteUser(path);
    const route = ROUTES[path] || ROUTES["404"];
    window.history.pushState(null, "", path);
    render(route);
  }

  function hashNavigator(hash) {
    hash = hash || window.location.hash;
    hash = validateHashRouteUser(hash);
    const route = HASH_ROUTES[hash] || HASH_ROUTES[404];
    window.history.pushState(null, "", hash);
    render(route);
  }

  return {
    router() {
      if (window.location.hash) {
        hashNavigator();
      } else {
        navigator();
      }
    },
    navigator,
  };
}

이 코드에서 찾은 문제점은 아래와 같다.

  1. 사용자 로그인 검증을 하고 있다.
  2. 경로 기반 라우터와 해시 기반 라우터의 분기처리를 하고 있다.
  3. 라우터 경로에 대한 페이지 설정이 하드 코딩되어 있다.

2번은 라우터의 역할이 아닌가 고민해봤는데 실제로 react-router-dom을 사용할 때, 브라우저 라우터를 사용할지 해시 라우터를 사용할지는 개발자가 별도로 선언하여 사용하는 걸로 알고 있어서 외부로 분리하기로 했다.

let routes = {};
let hashRoutes = {};

export function createRouter() {
  function addRoutes(path, element) {
    routes = { ...routes, [path]: element };
  }

  function addHashRoutes(path, element) {
    hashRoutes = { ...hashRoutes, [path]: element };
  }

  function render(pageComponent) {
    const root = document.getElementById("root");
    root.innerHTML = pageComponent();
  }

  function navigator(path) {
    const pageComponent = routes[path] || routes["404"];
    window.history.pushState(null, "", path);
    render(pageComponent);
  }

  function hashNavigator(hash) {
    const pageComponent = hashRoutes[hash] || hashRoutes["404"];
    window.history.pushState(null, "", hash);
    render(pageComponent);
  }

  return {
    navigator,
    hashNavigator,
    addRoutes,
    addHashRoutes,
  };
}

위처럼 수정하고 나니 고민이 또 생겼다.

  1. 경로 기반 라우터와 해시 기반 라우터 내부 로직이 똑같은데, 의미상 나누는 게 맞을까?
  2. 모듈로 분리하긴 했지만 routeshashRoutes를 모듈 상의 전역 변수로 선언하는게 맞을까?

이 고민 역시 다른 분들의 의견을 들어보고자 슬랙에 올렸다.

슬랙에 올린 질문

정성스럽게 댓글들을 달아주셨는데(다시 한 번 감사합니다), 댓글의 내용을 읽고 '왜 createRouter라는 이름을 사용하고 함수로 내보내고 있을까?'라는 생각이 들었다.
createRouter라는 이름의 함수는 const router = createRouter();처럼 사용할 것 같지만 막상 내보내는 반환값은 addRoutesnavigator를 메서드로 갖는 객체를 반환한다.
이 객체를 router 변수에 담고 사용할 수도 있지만 변수명은 다르게 작성할 수도 있고 구조 분해 할당으로 받는다면 더욱 이름의 의미가 퇴색될 것 같다.

createRouter.jsRouter.js로 변경하고 내부를 싱글톤 패턴의 클래스로 작성했다.
이로써 2번에서 고민한 내용은 클래스의 필드값으로 갖게 되어 고민을 해소할 수 있었다.

하지만 우연히 책을 보다가 발견한 내용인데, "모듈에서 공개적으로 내보내진 메서드는 내부 모듈 세부 사항에 대한 클로저를 유지한다. 이를 통해 프로그램이 살아 있는 동안 모듈 싱글톤의 상태가 유지된다."는 내용이었다.

'모듈로 분리한 것부터 싱글톤이면 굳이 클래스를 사용할 필요가 없겠네?'

let routes = [];
let target = null;

function render(route) {
  target.innerHTML = route.element();
}

export function setRenderTarget(element) {
  target = element;
}

export function addRoutes(path, element) {
  routes = [...routes, { path, element }];
}

export function navigator(path) {
  const route =
    routes.find((route) => route.path === path) ||
    routes.find((route) => route.path === "*");
  window.history.pushState(null, "", path);
  render(route);
}

render 함수를 라우터 로직에서 분리하고 싶었는데 여러 방법을 고민하다가 가만히 두기로 했다.

첫 번째로 'render 함수 자체를 다른 파일로 분리할까?'라고 고민했지만 라우터에서만 사용하고 있어서 큰 의미가 있을까 싶어서 패스.

두 번째로 '옵저버 패턴처럼 사용할까?'라고 생각했는데, 구현을 하다보니 라우터에 naviagtor 실행 후 실행할 함수를 콜백으로 받아야 하는데, 'render가 라우터의 역할에 맞지 않아서 분리하는 건데 render를 빼고 subscribe 함수를 넣는 건 역할에 맞나?'라는 고민이 생겨서 패스.

결국 render할 대상을 외부에서 주입받도록 하는 걸로 타협했다.

let target = null;

function render(pageComponent) {
  target.innerHTML = pageComponent();
}

export function setRenderTarget(element) {
  target = element;
}

💵 레이아웃 만들기

render 분리에 실패했으니 다른 거라도 해야겠다는 생각으로 개선할 점을 찾아봤다.
MainPageProfilePage는 같은 레이아웃을 공유하고 있는데 별도로 호출하고 있어서 레이아웃을 만들기로 했다.

import Footer from "@/components/layout/Footer";
import Header from "@/components/layout/Header";

const Layout = (children) => `
  <div class="bg-gray-100 min-h-screen flex justify-center">
    <div class="max-w-md w-full">
      ${Header()}
      ${children()}
      ${Footer()}
    </div>
  </div>
`;

export default Layout;

먼저 레이아웃 컴포넌트를 작성한 뒤 addRoutes 함수를 바꿔줬다.

// main.js
addRoutes(
  {
    element: Layout,
    children: [
      { path: "/", element: MainPage },
      { path: "/profile", element: ProfilePage },
      { path: "#/profile", element: ProfilePage },
      { path: "#/", element: MainPage },
    ],
  },
  { path: "/login", element: LoginPage },
  { path: "#/login", element: LoginPage },
  { path: "*", element: NotFoundPage },
);

// router.js
export function addRoutes(...newRoutes) {
  routes = [...routes, ...newRoutes];
}

react-router-dom에서 레이아웃을 설정하는 코드를 보고 레이아웃을 공유할 route들을 children 속성으로 넣어줬다.
routes의 내용이 변경됐으니 navigator 함수도 변경해줘야 한다.
Layoutpath 속성을 넣게 되면 혹여나 path가 일치하는 상황이 발생할 수도 있을 것 같아서 넣지 않았다.

function findRoutes(path, routeList) {
  for (const route of routeList) {
    if (route.path === path) return route.element;
    if (route.children) {
      const layout = route;
      for (const child of route.children) {
        if (child.path === path) {
          return () => layout.element(child.element);
        }
      }
    }
  }
  return routeList.find((route) => route.path === "*").element;
}

export function navigator(path) {
  const pageComponent = findRoutes(path, routes);
  window.history.pushState(null, "", path);
  render(pageComponent);
}

navigator에서 pageComponent를 찾는 함수를 findRoutes 함수로 분리했다.
findRoutes 함수에서 path가 일치하면 element를 바로 반환하고 children 속성이 있다면 다시 반복문을 돌려 path를 검사한다.
routes의 모든 데이터를 순회해도 일치하는 pageComponent가 없다면 NotFoundPage를 반환하도록 했다.

💰 과제를 진행하며 아쉬웠던 점: 모르는 게 너무 많다.

과제를 진행하며 아쉬웠던 점은 과제에 대한 아쉬움보다 과제를 진행하며 느낀 나에 대한 아쉬움이다.
과제를 하다가 모르는 게 있으면 질문을 남기거나 zep에서 다른 항해원분들과 여쭤보고 있다.
다들 정성스럽게 대답을 해주시고 의견을 주셔서 정말 감사할 따름이다.
하지만 모르는 게 많다보니 대답해주신 의도를 잘 파악했나 싶을 때가 있다.
댓글에 답글을 모두 달고 있지만, 답글을 달고 매번'아 이 얘기가 아니셨나?'하는 생각이 든다.

💵 vitest와 git commit 컨벤션

첫 발제일에 zep에서 다른 분들과 과제를 하고 있었다.
과제의 통과 기준은 테스트 코드 여부이다.
요구 사항을 보고 그냥 구현하자니 생각이 많아지고 어떻게 접근해야 할지 막막했다.
따라서 테스트 코드 1번부터 하나씩 통과하는 전략으로 바꿔 진행했다.

하지만 이 전략도 테스트 코드 1번 부터 차례대로 실행하고 싶은데 npm run test 명령어를 실행하면 테스트 코드 전체가 실행되어 어려움을 겪었다.
그러다 학습 메이트 분께서 VSCODE의 확장 프로그램인 vitest를 알려주셨다.
덕분에 테스트 코드를 하나씩 실행하며 과제를 진행할 수 있었다.

추가로 다른 분께서 화면 공유를 키고 과제를 진행하고 계셨는데 학습 메이트 분께서 "깃 커밋 메세지 저렇게 남겨주시면 너무 좋아요."라고 말씀하셔서 화면을 염탐했다.
그 분의 커밋 메세지를 보고 구글에 "깃 커밋 컨벤션"을 검색하여 커밋 메세지를 작성했다.

주변에 개발자가 없어서 그런걸까 누구에게 당연한 것도 모르고 있다는 사실에 조금 위축됐다.
마음 같아서는 잘 하는 분 옆에 찰싹 달라붙어 무슨 단축키를 사용하는지, 코드를 짤 때는 어떤 생각을 하는지, 키보드는 어떤 걸 쓰는지 하나하나 들여다 보고 싶다.
낯을 가린다고 혼자 고민하기 보다 여러 곳에 나를 노출시켜야 할 필요성을 느꼈다.

💵 예리하지 않은 감각

과제를 진행하면서는 "테스트 코드 통과"에 중점을 뒀다.
일찍 시작해서 조금 이르게 과제를 끝내고 코드가 지저분하다고 생각돼 리팩토링을 진행하고 있었다.
그러다 코어 타임(우리팀과 협력팀이 특정 시간에 zep에 접속하여 활동하는 시간)에 다른 분들과 과제 얘기를 했다.

다른 분들은 단순히 과제를 통과하는 것이 아니라 확장성을 고려하고 과제의 의도를 파악하고 설계를 하고 계셨다.
나는 생각하지 못했던 점들을 캐치해서 고민하는 모습에 '나는 왜 예리하게 느끼지 못했을까'라는 생각을 했다.

하지만 돌이켜 생각해보면 천천히 고민하면서 풀었어도 그런 고민을 했을까 싶다.
프론트엔드 프로젝트 경험이 없다보니 경력이 있으신 분들에 비해 생각할 풀도 적을 수 있고(합리화일 수 있고) 오히려 과제를 빨리 마친 덕분에 시간을 갖고 천천히 리팩토링을 진행할 수 있었다.

과제 중간 Q&A 시간에 멘토님께 코드를 짜기 전 어떤 동작을 할 거다라고 생각한 뒤 어떻게 코드를 작성해야겠다는 판단이 늘기위해 공부할 책을 추천해주실 수 있는지 질문드렸다.

멘토님께서 다음 챕터인 클린 코드의 내용이 들어간 "쏙쏙 들어오는 함수형 프로그래밍"책을 추천해주셨다.

💵 테스트 코드

나는 무려 테스트 코드도 작성해본 적이 없다.
자랑은 아니지만 이번에 과제를 보면서 E2E 테스트라는 것도 처음 알았다.
테스트 코드를 실행시키면서 오류가 발생했을 때 어떤 동작을 기대하는지 코드를 보고도 몰랐던 경우가 있었다.
항해 과정 중에 테스트 코드 챕터가 있지만 그 전에 한 번 공부해야겠다는 생각이 들었다.
마침 좋은 강의도 추천받았다.

인프런 프론트엔드 테스트 강의

이 강의도 다다음 챕터인 테스트 코드의 내용이 들어가있다고 한다.
챕터가 들어가기 전에 완강을 목표로 해야겠다.

@wonjung-jang wonjung-jang marked this pull request as draft December 16, 2024 06:32
Copy link

@CodyMan0 CodyMan0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

궁금해서 여쭤봐요~

src/main.js Outdated
document.body.innerHTML = router(path);
}

window.addEventListener("popstate", updateContent);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

원정님 혹시 popstate와 load 이벤트를 윈도우에 걸어놓은 이유가 무엇일까요?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updateContent 함수를 호출하면 동일한 효과일 것으로 예상되는데 궁금해서 여쭤봐요

Copy link
Author

@wonjung-jang wonjung-jang Dec 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

안녕하세요 주영님!

load는 페이지 초기 로딩 시에 페이지 컴포넌트를 불러오기 위해서 이벤트 리스너를 추가했습니다!

popstate는 브라우저의 히스토리를 감지하는 이벤트인데요.

image

브라우저 상단에 "뒤로 가기", "앞으로 가기", "새로고침"을 감지합니다.
추가로 설명 드리기 위해 좀 더 찾아봤더니 history.back(), history.forward(), history.go() 메서드 호출 시에도 트리거 된다고 합니다.

정리하면 load는 페이지 초기 로딩 및 새로고침, popstate는 뒤로, 앞으로 가기 버튼에 대응하고자 추가했습니다.

제 코드를 관심갖고 봐주셔서 감사해요!😁

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

원정님 친절한 설명 감사드려요 :) 이해했습니다

@wonjung-jang wonjung-jang changed the title 11팀 장원정 과제 제출합니다. [11팀 장원정] [Chapter 1-1] 프레임워크 없이 SPA 만들기 Dec 16, 2024
@wonjung-jang wonjung-jang marked this pull request as ready for review December 20, 2024 00:08
Copy link

@osohyun0224 osohyun0224 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

안녕하세요 원정님 :) 담당 학습메이트 오소현입니다 🫶🏻

PR 템플릿을 넘어 원정님께서 학습하신 내용을 PR에 너무 잘 작성해주신 것 같아요! 정리 너무 잘하시네요 bb 요번주 과제의 가장 핵심 개념은 이벤트 위임 개념인 것 같은데 기술적 성장 부분에 초반부터 개념을 잘 정리해주셔서, 리뷰하는 입장에서 이해가 잘 되었어요 :)

또한 과제하면서 고민한 내용도 각 이슈별로 고민했던 타임라인과 점점 발전해나가신 과정을 자세히 기록해주셔서 원정님께서 한 주간 이과제를 얼마나 열심히 몰입해서 하셨고, 점점 다듬어져가는 코드를 만들어 나가시는 과정을 볼 수 있었습니다 :) 너무 잘하셨습니다 읽으면서 다음내용이 기다려지는 PR 리뷰였습니다 ㅎㅎ

마지막으로 원정님의 과제 회고, 느낌과 생각을 잘 정리해주셔서 즐겁게 리뷰할 수 있었습니다 :) 시간 투자해주셔서 PR 작성해주셔서 너무 감사하고, 이러한 과정이 원정님께서 더 잘 성장 하실 수 있을거라고 생각됩니다! 첫 주차부터 정말 잘해주셨습니다 bb


원정님의 코드를 보면서 궁금한 내용, 더 제안드려보면 좋을 내용을 부족한 실력이지만 조금이나마 작성해보았습니다 bb 완벽하게 설명된 PR을 읽으니까 원정님의 의도도 잘 보이고, 더 제안해보고 싶은 내용이 술술 떠올랐어요! 리뷰가 참 재밌었습니다 bb 제가 코드에 직접 리뷰 담긴 내용 찬찬히 읽어보시고 좋은 인사이트가 되셨기를 바랍니다😎

이번주차 정말 고생많으셨습니다 bb 주말부터 착착 진행하시고, 다른 팀 분들과도 함께 고민하면서 문제를 해결해 나가아시는 모습이 너무 멋있었습니다 bb 원정님의 좋은 동료가 되고 싶네요 :)
2주차 과제도 화이팅입니다🍀

const Header = () => {
const isLogin = userStore.isLogin();
const path = window.location.pathname;
// 석호님 코드 따라하기

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ㅋㅋㅋㅋㅋ 귀엽네요 원정님 :) 다른 분들 PR 열심히 보시는 것도 너무 잘 하고 계십니다 !

"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alias 사용해주신 것 너무 좋네요 👍 훨씬 더 코드가 깔끔해졌어요 !

원정님 폴더 구조를 보면 /src아래에 components, pages, router, store, utils등 다양하게 폴더가 잘 나누어져 있는데 요라인까지 alias를 낮춰보면 어떨까 싶어요!

예를 들면

      '@components': path.resolve(__dirname, './src/components'),
      '@pages': path.resolve(__dirname, './src/pages'),
      '@router': path.resolve(__dirname, './src/router'),
      '@store': path.resolve(__dirname, './src/store'),
      '@utils': path.resolve(__dirname, './src/utils'),

다음 과제에서 폴더가 늘어날 때는 한번 도전해 보시져!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

감사합니다 ㅎㅎ 준만님께서 알려주셨어요!
말씀하신 내용 보니까 제가 alias를 쓰도록 해놓고 활용을 잘 하지 못한 것 같네요!
다음 과제 때는 좀 더 활용해보겠습니다!

import Footer from "@/components/layout/Footer";
import Header from "@/components/layout/Header";

const Layout = (children) => `

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 평소에 컴포넌트 폴더 내부의 Layout에는 어떤게 해당될까 고민이 있었는데요!

고민 끝에 레이아웃에 해당하는 기준은 서비스 내부 로직이나 데이터에 의존하지 않아야한다 라는 특징을 가지고 있는 컴포넌트 요소들을 분류해두기로 했어요
예를 들면 헤더 푸터 제외하고도 사이드 바나 네비게이션, 전체 템플릿이 있을 것 같아요!
저도 요번 과제에서는 Header, Footer, 전체 포괄하는 레이아웃이 이에 해당한다고 생각되네용 ㅎㅎㅎ

원정님은 어떤 기준으로 레이아웃 폴더에 들어갈 컴포넌트들을 구분하시나용??

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 Layout의 역할을 생각하기 보다 단순하게 MianPageProfilePage가 동일한 레이아웃을 갖고 있어 공통으로 묶는 작업을 한다고 생각했었습니다.
소현님께서 고민하신 내용 들어보니 코드를 짜기 전 역할에 대해서 생각하고 고민하는 과정을 가져야겠다는 생각이 드네용 ㅎㅎ

레이아웃 폴더에는 레이아웃과 레이아웃에 들어가는 컴포넌트를 같이 넣었습니다.
레이아웃 폴더에 헤더랑 푸터 외에 다른 컴포넌트가 많아진다면 폴더를 따로 분리했을 텐데 두 가지라서 그냥 한 폴더에 넣어놨습니다.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wonjung-jang 오호~~! 자세하게 답변해주셔서 감사합니다 :)

Comment on lines +27 to +39
function validateRouteUser(path) {
const isLogin = userStore.isLogin();
if (!isLogin && path === "/profile") path = "/login";
if (isLogin && path === "/login") path = "/";
return path;
}

function validateHashRouteUser(hash) {
const isLogin = userStore.isLogin();
if (!isLogin && hash === "#/profile") hash = "#/login";
if (isLogin && hash === "#/login") hash = "#/";
return hash;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 원정님! 해시 라우트와 일반 라우트 유효성 검사하시는 로직을 따로 분리해서 함수로 작성해주셨네요 bb
각 함수의 네이밍도 적절하게 잘 해주신 것 같습니다 bb 확실하게 두 함수가 각자 어떤 역할을 하는지 파악할 수 있었어요!

만약에 제가 구현해본다면 저는 두 함수의 로직이 비슷해 보이네요 :) 그래서 두 함수를 하나로 합치고, 인자로 경로와 타입을 받아 처리할 수 있도록 할 것 같아요! 함수의 재사용성이 조금 높아 보이는 제 코드로 다음과 같이 제안해볼게요 !

Suggested change
function validateRouteUser(path) {
const isLogin = userStore.isLogin();
if (!isLogin && path === "/profile") path = "/login";
if (isLogin && path === "/login") path = "/";
return path;
}
function validateHashRouteUser(hash) {
const isLogin = userStore.isLogin();
if (!isLogin && hash === "#/profile") hash = "#/login";
if (isLogin && hash === "#/login") hash = "#/";
return hash;
}
function validateAndRedirectRoute(route, type) {
const isLogin = userStore.isLogin();
if (type === 'hash') {
if (!isLogin && route === "#/profile") route = "#/login";
if (isLogin && route === "#/login") route = "#/";
} else {
if (!isLogin && route === "/profile") route = "/login";
if (isLogin && route === "/login") route = "/";
}
return route;
}

Comment on lines +41 to +51
function branchRoute() {
let { hash, pathname } = window.location;
if (hash) {
hash = validateHashRouteUser(hash);
navigator(hash);
} else {
pathname = validateRouteUser(pathname);
navigator(pathname);
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
function branchRoute() {
let { hash, pathname } = window.location;
if (hash) {
hash = validateHashRouteUser(hash);
navigator(hash);
} else {
pathname = validateRouteUser(pathname);
navigator(pathname);
}
}
function branchRoute() {
let { hash, pathname } = window.location;
let route = hash || pathname;
let type = hash ? 'hash' : 'path';
route = validateAndRedirectRoute(route, type);
navigator(route);
}

고럼 여기 branchRoute() 함수도 위와 같이 더 바뀔 수 있을 것 같아요 bb

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오... 감사합니다!
안 그래도 둘이 비슷한 로직을 수행하는데 나눠져 있어서 어떻게 해야하나 고민이 있었는데, 소현님께서 작성해주신 코드 보니 branchRoute 함수도 더 깔끔하고 이해하기 좋게 바뀐 것 같아요!👍
많이 배우겠습니다. 감사합니다!

Comment on lines +10 to +24
<div class="bg-white rounded-lg shadow p-4">
<div class="flex items-center mb-2">
<img src="https://via.placeholder.com/40" alt="프로필" class="rounded-full mr-2">
<div>
<p class="font-bold">홍길동</p>
<p class="text-sm text-gray-500">5분 전</p>
</div>
</div>
<p>오늘 날씨가 정말 좋네요. 다들 좋은 하루 보내세요!</p>
<div class="mt-2 flex justify-between text-gray-500">
<button>좋아요</button>
<button>댓글</button>
<button>공유</button>
</div>
</div>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

페이지는 모두 UI만을 담당하고 있군요 bb 이렇게 확실하게 분리해주시는거 너무 좋은 것 같아요bb

원정님께서 컴포넌트 폴더를 따로 생성해 분리해주셔서, 저는 요런 페이지들에 공통된 UI 컴포넌트들을 따로 공통 컴포넌트로 분리해 볼 것 같아요!

요런 게시글 컴포넌트를 따로 분류하고, 댓글 데이터는

  {
    name: "홍길동",
    time: "5분 전",
    message: "오늘 날씨가 정말 좋네요. 다들 좋은 하루 보내세요!",
    avatar: "https://via.placeholder.com/40"
  },
  {
    name: "김철수",
    time: "15분 전",
    message: "새로운 프로젝트를 시작했어요. 열심히 코딩 중입니다!",
    avatar: "https://via.placeholder.com/40"
  },
  {
    name: "이영희",
    time: "30분 전",
    message: "오늘 점심 메뉴 추천 받습니다. 뭐가 좋을까요?",
    avatar: "https://via.placeholder.com/40"
  }
];

이렇게 정의해서 가져와 사용하는 구조도 깔끔해질 것 같아서 제안드려 봅니다~!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오.. 제가 페이지 컴포넌트는 분리해놓고 생각을 안 했는데, 저렇게 하면 Item 컴포넌트를 만들어서 반복문을 사용해서 활용할 수 있겠네요!👍

Comment on lines +12 to +25
function findRoutes(path, routeList) {
for (const route of routeList) {
if (route.path === path) return route.element;
if (route.children) {
const layout = route;
for (const child of route.children) {
if (child.path === path) {
return () => layout.element(child.element);
}
}
}
}
return routeList.find((route) => route.path === "*").element;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이야 이 findRoutes 함수 너무 잘 구현해주신 것 같아요 !
본 기능인 경로를 찾아내는 기능도 잘하고 있고, 특히 중첩 라우트를 처리하는 로직까지 있어서 코드가 보다 유연해졌네요 bb 많은 고민의 흔적이 느껴집니다,,, 👍

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Layout 기능을 넣고 싶어서 layouts 변수를 만들어서 별도로 담을까 고민하다가, react-router-dom에서 레이아웃을 설정할 때

<Routes>
  <Route element={<Layout/>}>
    <Route path="/" element={<MainPage/>}/>
    ... 다른 페이지들
  </Route>
</Routes>

이런식으로 쓰는 걸 보고 컴포넌트도 함수고 자식 컴포넌트를 인자로 받는다고 하면 어떻게 구현하면 좋을지 고민했습니다.

그래서 addRoutes에서 중첩 라우트?를 받아서 저장하고, 저장한 데이터를 처리할 수 있게 findRoutes 함수를 작성했어요 ㅎㅎ

고민한 부분을 알아주셔서 좋네요 ㅎㅎ 감사합니다!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wonjung-jang 오!! react-router-dom의 구조를 참고하셨군요,,👀 이 과제의 목적에 잘 부합하는 코드인 것 같네요,,,👍🏻👍🏻 아주 좋습니다 !!

Comment on lines +7 to +17
if (tagName === "A") {
e.preventDefault();
const { href } = e.target;
let path = new URL(href).pathname;
if (id === "logout") {
userStore.deleteUser();
path = "/login";
}
navigator(path);
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

굿굿 clickEventHandler 함수에서 각 역할을 간결하게 잘 작성해주셨네요!
혹시 logout 로직을 별도로 분리해보는 건 어떨까요?

function handleNavigation(href, id) {
  let path = new URL(href).pathname;
  if (id === "logout") {
    userStore.deleteUser();
    path = "/login";
  }
  navigator(path);
}

요런 식으로 로그아웃 로직을 별도로 분리하는 것도 코드 가독성이 높아질것 같아요!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

안 그래도 분리를 할까 고민했었는데 let path = new URL(href).pathname; 코드와 navigator(path); 코드가 동일하게 사용돼서 분리한다면 if (id === "logout") { userStore.deleteUser(); path = "/login"; } 여기만 분리해야 할 것 같은데 내용이 짧아서 그냥 합쳤었는데요.

다시 생각해보니 의미상으로도 그렇고 분리하는 게 좋을 것 같네요!
지금 다시 한다면

function logout(path, id){
  if (id === "logout") { 
    userStore.deleteUser(); 
    path = "/login"; 
  }
  return path;
}
if (tagName === "A") {
  e.preventDefault();   
  const { href } = e.target;
  let path = new URL(href).pathname;
  path = logout(path, id);
  navigator(path);
}

함수명이 별로지만.. 이런식으로 할 것 같기도 하네요!
감사합니다!

Copy link
Author

@wonjung-jang wonjung-jang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

소현님 정성스러운 코드리뷰 감사합니다!😁
초반에 소현님께서 많이 알려주시고 PR 작성하신 것도 보여주셔서 잘 남길 수 있었습니다(소현님 블로그 많이 참고했어요!). ㅎㅎ

리뷰 내용보니 제가 고민한 것들에 대해서 짚어주시고 미처 생각하지 못한 부분도 말씀해주셔서 감사합니다! ㅎㅎ
좋은 인사이트 많이 얻었습니다!

첫 주차에 받은 기대는 감사한 마음으로 받고 10주동안 유지해보겠습니다!
저도 소현님의 좋은 동료가 될 수 있게 열심히 하겠습니다!
감사합니다!!👍

}
}
return routeList.find((route) => route.path === "*").element;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

findRoutes는 진짜 원정님의 고민이 보이는 부분인것 같아요...!
특히 dx적인 측면에서 react router dom을 참고하셨다고 하셨는데, 비슷한 방식으로 사용할 수 있다는 점이 너무너무 멋지게 느껴집니다..ㅎㅎ 코드 잘 보고갑니다 원정님!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

안녕하세요 영서님! 제 코드를 좋게 봐주셨다니 감사합니다!😁

if (id === "profile-form") {
updateProfile(formData);
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

submitEventHandler를 분리한 부분이 좋네요!
그런데 현재 파일에서는 유틸인 submitEventHandler의 파일명을 채택하고는 있지만...하위에 login과 updateProfile라는 도메인이 짙은 함수들이 들어와 있는 것 같습니다...!

공통 이벤트 핸들러를 만들고, 그 이벤트 핸들러의 네임스페이스를 둔다던가 해서 분리를 하면 어떨까요? 🤔

const formHandlers = {
  'login-form': handleLoginSubmit,
  'profile-form': handleProfileSubmit
};
export function submitEventHandler(e) {
  e.preventDefault();
  const form = e.target;
  const handler = formHandlers[form.id];
  
  if (handler) {
    handler(new FormData(form));
  }
}

혹은 저렇게 공통 이벤트 핸들러를 만들고 사용하는 페이지에서 login과 updateProfile를 선언하여 사용하는 방법도 있을 것 같은데,
원정님의 생각도 궁금합니다 ㅎㅎ

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오!! 저는 생각지도 못했는데, 영서님께서 말씀하신 내용대로 바꾸면 이벤트 핸들러가 추가되거나 삭제되도 submitEventHandler는 그대로 있고 formHandlers에만 추가, 삭제를 하면 되겠네요!

마지막에 말씀하신 내용은 formHandlers라는 상태를 만들고 안에 들어가는 핸들러들은 페이지에 각각 선언해서 사용하는 방법을 말씀하신 걸까요?

그 방법이 맞다면 더 좋은 방법인 것 같아요!
그렇게 생각하는 이유는

  1. login, updateProfile은 각 페이지에서만 사용하고 다른 페이지에서 사용하지 않을 것 같아서
  2. 만약에 관리하는 핸들러가 늘어난다면 어디서 사용하는 핸들러인지 직관적이지 않고 매번 id값을 찾아야 하는 불편함이 있을 것 같아서

입니다.

오.. 너무 좋은 의견 감사해요!👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants