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

[12팀 정석호] [Chapter 1-1] 프레임워크 없이 SPA 만들기 #4

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

Conversation

creco-hanghae
Copy link

@creco-hanghae creco-hanghae commented Dec 14, 2024


안녕하세요, 4기 12팀 정석호입니다. 잘 부탁드립니다. 🙃 🙌


과제 체크포인트

기본과제

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) 이벤트 위임

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

과제 셀프회고

기술적 성장

  • 바닐라 JS 라는 용어가 나오기 전부터도 JS 로 프로젝트를 해본 적은 있었으나 React 가 세상에 나온 후 간단한 Browser 함수들을 이용해서 화면을 그려보는 순수 컴포넌트는 해본 적이 었었습니다.
  • 그런데 History API 를 통한 Hash Router 작업이나 Observer 패턴를 실제 상황에 맞게 도입해본 적 없이 들어만 봐왔는데, 이번 기회에 시도해보고 싶은 기술들 다 시도해보면서 TestCase를 통과해내는 결과를 낸 것 같아 기쁩니다.
  • History / Hash Router 를 구분짓지 않고 동시에 처리되도록 작업했습니다.
    • 따로 Hash 에서만 동작되는 등의 분기 처리가 존재하지 않으며 Hash (#) 값을 떼기 위한 처리만 했습니다.
  • 다른 의존성을 갖지 않도록 Router / Store 에 Observer 패턴을 도입하여 App 의 render 처리를 쉽게 할 수 있도록 구성했습니다.
  • 특히 이벤트 위임도 이야기만 들어봤지 실제로는 써본 적이 없었는데 이참에 사용해서 하위 컴포넌트가 렌더링이 끝나기 전에 루트에 이벤트를 등록 하는 등의 장점을 느낄 수 있었습니다. 다만 TestCase에서 SubmitEvent 를 직접 dispatch 하는 등의 코드가 있어 폼 제출 submit 이벤트에는 위임할 수 없어서 아쉬웠습니다.
  • 그리고 Observer 패턴을 처음 쓰고자 하니 생각보다 그 역할이 모호하였는데, 일단 패턴 자체에 집중하기 보다는 그냥 니즈에 맞춰서 좀 적용해보니 이제 감이 좀 잡히기 시작했습니다.

코드 품질

👍 HoF + Observer 패턴

  • Observer 패턴에서 필요한 함수인 subscribe / notify / update 를 쓸 수 있는 객체로 만들어주는 고차 함수를 작성했습니다.
export function withObserver(instance = {}) {
  const observers = [];

  instance.subscribe = function (observer) {
    observers.push(observer);
  };

  instance.notify = function () {
    observers.forEach((x) => x.update(instance));
  };

  return instance;
}
  • get / set / remove 와 같이 명료하고 쉬운 스토어 구조를 가져봤습니다.
import { withObserver } from "../hof/withObserver";

export const store = withObserver({
  get: (key) => {
    return JSON.parse(localStorage.getItem(key));
  },
  set: (key, value) => {
    localStorage.setItem(key, JSON.stringify(value));
    store.notify();
  },
  remove: (key) => {
    localStorage.removeItem(key);
    store.notify();
  },
});

과제 피드백

  1. 테스트 계속 돌려볼 수 있는 거 너무 좋았습니다.

리뷰 받고 싶은 내용

Observer 패턴

  • 현재 notify 와 update 함수가 여기저기서 쓰이면서 무한 렌더링이 되거나 페이지 이동할 때 마다 notify 하는 횟수가 늘어나는 등의 이슈가 존재합니다.
    • 아마 App <-> Router <-> Store 사이에서 변경된 이벤트를 기준으로 다른 모듈의 update 동작이 되고 이벤트 함수가 계속 등록되는 등의 이슈로 보입니다.
    • 보통 Observer 패턴을 도입하면 만나게 되는 문제들은 또 무엇이 있을까요?
    • Observer 패턴으로 시스템을 구성하려면 어떤 방식의 설계가 필요할까요?
      • App <-> Router <-> Store 의 구조에 녹이는 데에 많이 힘들었고 사실 지금도 구멍이 있는 설계 같아 아쉽습니다.

creco-hanghae and others added 25 commits December 15, 2024 07:59
@creco-hanghae
Copy link
Author

그만달아요~ :(

@creco-hanghae
Copy link
Author

😞

@creco-hanghae creco-hanghae changed the title Chapter 1-1 기본과제 Draft [12팀 정석호] [Chapter 1-1] 프레임워크 없이 SPA 만들기 Dec 19, 2024
@creco-hanghae creco-hanghae marked this pull request as ready for review December 19, 2024 19:02
import { Footer } from "../components/Footer";

export const MainPage = {
register: ({ router }) => {
Copy link

Choose a reason for hiding this comment

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

석호님! 저 질문이 있는데 register와 render 메서드를 분리해서 구현하신 거 같은데 혹시 어떤 이유에서 하신 건지 궁금해요!

Copy link
Author

Choose a reason for hiding this comment

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

render 하나로 퉁쳤었는데, 이벤트 등록 때문에 분리 했습니다.
사실 #root 에 모두 위임해두면 render 하나로 퉁 칠 수 있습니다.

그런데 테스트 코드 구현체에서 submit 을 직접 참조해서 이벤트를 dispatch 하는 경우가 있어서 이벤트 위임으로 해결되지 않은 이벤트가 있었고, 반드시 document.body 에 DOM 이 생성되고 나서 이벤트를 등록해야하는 상황이 발생했습니다.

그래서 어쩔수 없이 그냥 render 와 resgister 를 분리하기로 했습니다 ㅠ 울며 겨자먹으며

Copy link

@pangkyu pangkyu left a comment

Choose a reason for hiding this comment

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

먼저 구현완료하시고 추가로 옵저버패턴 적용하신다고 리팩토링하시는 거에 감명받았습니다..🔥
이번주 구현 작업하시느라 고생하셨습니다!


export function createApp({ renderCurrentPage }) {
function render() {
document.getElementById("root").addEventListener("click", (e) => {
Copy link

Choose a reason for hiding this comment

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

다른 리스너들과는 다르게 앵커를 app.js에서 선언해주신게 전체적인 페이지에서 헤더를 쓰고 있어서 선언해두신게 맞을까요?? 그러면 전역적으로 사용되는 이벤트리스너들이 있다고 판단하면 렌더 단계에서 모두 선언하면 되는 지가 궁금합니다!

Copy link
Author

@creco-hanghae creco-hanghae Dec 21, 2024

Choose a reason for hiding this comment

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

넵 생각하신대로 "a 태그를 누르면 페이지 이동이 SPA 처럼 된다"라는 기능이 페이지 종속적인 기능이 아니라서 전역에 처리해준 게 맞습니다. 방법은 다양할 것 같긴 해유. 어차피 이벤트 위임으로 #root 만 있으면 동작하는 이벤트라 렌더링 맨 위에 올려둬도 문제가 없기도 하고 // 다 로드되고 이벤트 등록해고 괜찮을 것 같긴 합니다

Comment on lines +1 to +13
export function withObserver(instance = {}) {
const observers = [];

instance.subscribe = function (observer) {
observers.push(observer);
};

instance.notify = function () {
observers.forEach((x) => x.update(instance));
};

return instance;
}
Copy link

Choose a reason for hiding this comment

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

withObserver 하나로 페이지 렌더링, 라우터, 스토어 구독을 전부 해결하신 거군요...! 👍🏻

@creco-hanghae
Copy link
Author

(기록용)

코치님 코멘트

  • 이미 로컬스토리지에 저장하기때문에 persistent한데 왜 { persistent: true },를 추가한건지 궁금합니다.

스토어에 persistent 기능이 들어간 게 사실 이후에 추가한 동작인데, 생각해보니 persistent 기능만 쓰고 있어서 기본으로 내장했었어요! 그런데 마지막에 기본으로 내장해놓고 파라미터를 지우는 걸 잊었네요 🥲 감사합니다!

  • 상태가 변경되지 않아도 nofity가 호출될수 있는 가능성이 지금은 있어보여요. 상태 변경이 진짜 필요한 경우만 notify가 호출되도록 리팩토링 진행을 권유드려요.

오,,!! 맞습니다. 지금은 일단 그냥 set / clear 함수가 호출 되기만 하면 notify 가 호출되네요! 감사합니다 :)

@creco-hanghae
Copy link
Author

(기록용)

  • 자잘한 리뷰 사항은 PR템플릿에 접기기능 쓰지말아주시고(수십개 문서 읽거나 리뷰해보시면 무슨말인지 알게되세요) '테케'같은 축약어는 not 프로페셔널해보이니 타인들과 공유하는 문서에선 가급적이면 정식 명칭을 써주세요. 이런 자잘한거에서 리더로서의 자질이 보이는거니까요.

말씀하신대로 PR 문서를 잘 가꿔두면 좋겠네요! 다음 PR 부터는 좀 더 잘 작성해볼게요! 👍 👍 👍

@Violet-Bora-Lee
Copy link

회고까지 또 하시고.. 최고입니다 👍🏼👍🏼 저도 배울게요🙏🏿🙏🏿

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.

6 participants