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

[13팀 김보영] [Chapter 1-1] 프레임워크 없이 SPA 만들기 #19

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
## 과제 체크포인트

### 기본과제

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

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

#### 3) 프로필 페이지 구현:
- [x] 현재 로그인한 사용자의 정보 표시
- [x] 사용자 이름
- [x] 간단한 소개
- [x] 프로필 수정 기능
- [x] 사용자 소개 텍스트 수정 가능
- [x] 수정된 정보 LocalStorage에 저장

#### 4) 컴포넌트 기반 구조 설계:
- [x] 재사용 가능한 컴포넌트 작성
- [x] Header 컴포넌트
- [x] Footer 컴포넌트
- [x] 페이지별 컴포넌트 작성
- [x] HomePage 컴포넌트
- [x] ProfilePage 컴포넌트
- [x] NotFoundPage 컴포넌트

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

#### 6) 이벤트 처리 및 DOM 조작:
- [x] 사용자 입력 처리 (로그인 폼, 프로필 수정 등)
- [x] 동적 컨텐츠 렌더링 (사용자 정보 표시, 페이지 전환 등)

#### 7) 기본적인 에러 처리:
- [x] 잘못된 라우트 접근 시 404 페이지 표시
- [x] 로그인 실패 시 에러 메시지 표시

### 심화과제

#### 1) 해시 라우터 구현
- [x] location.hash를 이용하여 SPA 라우터 구현
- [x] '/#/' (홈 페이지)
- [x] '/#/login' (로그인 페이지)
- [x] '/#/profile' (프로필 페이지)

#### 2) 라우트 가드 구현
- [x] 로그인 상태에 따른 접근 제어
- [x] 비로그인 사용자의 특정 페이지 접근 시 로그인 페이지로 리다이렉션

#### 3) 이벤트 위임

- [x] 이벤트 위임 방식으로 이벤트를 관리하고 있다.

## 과제 셀프회고
- 이번 과제를 통해 프레임워크 없이 SPA를 직접 구현하면서 라우팅, 상태 관리, DOM 조작 등 웹 애플리케이션의 핵심 원리를 이해할 수 있었습니다. 특히 History API와 해시 라우터를 모두 적용하며 차이점을 체감했고, LocalStorage를 활용한 사용자 데이터 관리와 이벤트 위임 방식도 학습할 수 있었습니다. 다만, 상태 관리와 라우터 로직의 확장성, 그리고 DOM 조작의 효율성에서 개선이 필요하다고 느꼈습니다. 전반적으로 코드의 가독성과 유지보수성을 높이는 방법을 고민하게 되었고, 이를 통해 기초를 더욱 탄탄히 다져야겠다는 다짐을 했습니다.

### 기술적 성장
- History API와 DOM 조작에 대해 이해할 수 있게 되었습니다.
- 테스트의 중요성을 깨달았습니다.

### 코드 품질
- 모든 테스트에 통과한거 자체가 만족스럽습니다.
- 히스토리와 해시 라우터를 관리하는 부분에서 좀 더 생산성 있는 로직으로 수정하고 싶습니다.
- 일단 이벤트와 UI를 나누어 가독성 좋게 만드려고 노력했습니다.

### 학습 효과 분석
- 테스트 케이스에 맞추면서 구현을 하다보니 DOM 흐름에 대한 이해를 좀 더 깊이 이해할 수 있었습니다.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"test:e2e:ui": "playwright test --ui",
"test:e2e:report": "npx playwright show-report",
"test:generate": "playwright codegen localhost:5173",
"prepare": "husky"
"prepare": "husky",
"webstorm-integration": "vitest --watch --reporter=dot --reporter=json --outputFile=.vitest-result.json"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
Expand Down
7 changes: 7 additions & 0 deletions src/components/Footer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const Footer = () => {
return `
<footer class="bg-gray-200 p-4 text-center">
<p>&copy; 2024 항해플러스. All rights reserved.</p>
</footer>
`;
};
34 changes: 34 additions & 0 deletions src/components/Header.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { PATHS } from "../router/routes.js";
import { getLocalStorage } from "../storage/storage.js";

export const Header = () => `
<header class="bg-blue-600 text-white p-4 sticky top-0">
<h1 class="text-2xl font-bold">항해플러스</h1>
</header>
<nav class="bg-white shadow-md p-2 sticky top-14">
<ul id="nav-ul" class="flex justify-around">
${userNav()}
</ul>
</nav>
`;

// 로그인 여부에 따른 렌더링
const userNav = () => {
const user = getLocalStorage("user") || "{}";
const path = window.location.hash
? window.location.hash.slice(1)
: window.location.pathname;

if (user.username) {
return `
<li><a href="/" class="${path === PATHS.MAIN ? "text-blue-600 font-bold" : "text-gray-600"}">홈</a></li>
<li><a href="/profile" class="${path === PATHS.PROFILE ? "text-blue-600 font-bold" : "text-gray-600"}">프로필</a></li>
<li><a href="/login" id="logout" class="text-gray-600">로그아웃</a></li>
`;
} else {
return `
<li><a href="/" class="text-blue-600 font-bold">홈</a></li>
<li><a href="/login" id="login-but" class="text-gray-600">로그인</a></li>
`;
}
};
31 changes: 31 additions & 0 deletions src/events/loginEvents.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { navigateTo } from "../router/router.js";
import { saveLocalStorage } from "../storage/storage.js";
import { PATHS } from "../router/routes.js";

export const loginEvents = () => {
const form = document.getElementById("login-form");
const emailInput = document.getElementById("username");
const passwordInput = document.getElementById("passwordInput");

// 로그인
form.addEventListener("submit", (e) => {
e.preventDefault();
const email = emailInput.value.trim();
const password = passwordInput.value.trim();

if (validateForm(email, password)) {
saveLocalStorage("user", { username: email, email: "", bio: "" });
navigateTo(PATHS.PROFILE);
}
});

// 유효성 검사
const validateForm = (email) => {
if (!email) {
alert("이메일을 입력해주세요.");
emailInput.focus();
return false;
}
return true;
};
};
42 changes: 42 additions & 0 deletions src/events/mainEvents.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { navigateTo } from "../router/router.js";
import { clearLocalStorage, saveLocalStorage } from "../storage/storage.js";
import { PATHS } from "../router/routes.js";

export const mainEvents = () => {
// 헤더 이동
document.addEventListener("click", (e) => {
const target = e.target.closest("a");
if (target) {
e.preventDefault();
e.stopPropagation();
const href = target.getAttribute("href");
if (href) {
navigateTo(href);
}
}
});

// 프로필 수정
document.addEventListener("submit", (e) => {
e.preventDefault();
if (e.target && e.target.id === "profile-form") {
const updatedUser = {
username: document.getElementById("username").value,
email: document.getElementById("email").value,
bio: document.getElementById("bio").value,
};

saveLocalStorage("user", updatedUser);
alert("프로필이 업데이트 되었습니다.");
}
});

// 로그아웃
document.addEventListener("click", (e) => {
if (e.target && e.target.id === "logout") {
e.preventDefault();
clearLocalStorage();
navigateTo(PATHS.LOGIN);
}
});
};
Loading
Loading