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

[15팀 정채은] [Chapter 1-1] 프레임워크 없이 SPA 만들기 #7

Open
wants to merge 7 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
101 changes: 101 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
## 과제 체크포인트

### 기본과제

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

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

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

## 과제 셀프회고
그동안은 코드는 고치고 짜 봤지만 프레임워크를 쓰지 않고 웹 페이지를 만드는 것에 대해서는 생각해본 적이 없었다.
document.location.href 를 조작하는 원시적인 방식 이외에도 기본 제공되는 방식을 사용해서
프레임워크 내부의 구현 방식을 직접 해볼 수 있다는 점이 신선했다.
코테와 기존 코드 유지보수에서 벗어나서 본질을 생각해 볼 수 있는 과제였다.
web과 자바스크립트의 기본적인 지식이 부족하다는 것을 느꼈다.

### 기술적 성장
- 새로 학습한 개념 : 라우터, pushState
- 프레임워크 메뉴얼을 사용하지 않고도 다양한 방법으로 구현을 시도해 본 점

### 코드 품질
- 심화 과제와 리팩토링을 아예 못해본 점이 아쉽다.
- svelte나 vue처럼 한 파일 안에 컴포넌트 HTML, 그에 딸린 메서드를 동시에 구현해서 유지보수가 쉬운 모양으로 만들고 싶었는데
이것 보다 깔끔하고 보기 쉬운 레이아웃(ex. html을 단순 String이 아닌 html 형식으로 표시하거나...)으로 변경해보았다면 좋았을 것 같다.
-

### 학습 효과 분석
- web페이지 이동 방식에 대해 이해.
- 주니어 실무에는 어떻게 적용할 수 있을지 잘 모르겠지만 (보통 프레임워크를 사용하고, 그 프레임워크 방식대로 구현하게 되므로)
후에 시니어가 되면 세팅을 하면서 써볼 수 있지 않을까 생각한다.

### 과제 피드백
- 라우터 기본 개념을 모르는 상태에서 과제를 하려니 처음 시작을 어떻게 해야 할지 막막했습니다.
- 도움되는 자료를 많이 올려주셨는데, 오히려 너무 많아서 정말 과제에 필요한 내용이 무엇인지와 알아두면 좋을 내용이 무엇인지 구분이 잘 안되었습니다.
-

## 리뷰 받고 싶은 내용
- 지금은 HTML코드가 단순 String으로 되어있는데, vs code에서 html형식으로 보려면 어떻게 수정하는 게 가장 빠르고 단순할까요?
- 컴포넌트가 많아지면 어떻게 정리를 하면 좋을지. 회사에서는 vue와 svelte를 써봤는데, 컴포넌트가 많아지면 많아질수록 메서드가 사방 팔방으로 흩어지고
재사용이 어려워진다고 느껴졌습니다. A페이지와 B페이지에서 공용으로 사용하는 컴포넌트(C)가 있다면 A 페이지와 B 페이지에서 사용되는 각각의 메서드들을 다른 회사에서는 어떻게 정리하는 편인지?
290 changes: 49 additions & 241 deletions src/main.js
Original file line number Diff line number Diff line change
@@ -1,241 +1,49 @@
const MainPage = () => `
<div class="bg-gray-100 min-h-screen flex justify-center">
<div class="max-w-md w-full">
<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 class="flex justify-around">
<li><a href="/" class="text-blue-600">홈</a></li>
<li><a href="/profile" class="text-gray-600">프로필</a></li>
<li><a href="#" class="text-gray-600">로그아웃</a></li>
</ul>
</nav>

<main class="p-4">
<div class="mb-4 bg-white rounded-lg shadow p-4">
<textarea class="w-full p-2 border rounded" placeholder="무슨 생각을 하고 계신가요?"></textarea>
<button class="mt-2 bg-blue-600 text-white px-4 py-2 rounded">게시</button>
</div>

<div class="space-y-4">

<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>

<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">15분 전</p>
</div>
</div>
<p>새로운 프로젝트를 시작했어요. 열심히 코딩 중입니다!</p>
<div class="mt-2 flex justify-between text-gray-500">
<button>좋아요</button>
<button>댓글</button>
<button>공유</button>
</div>
</div>

<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">30분 전</p>
</div>
</div>
<p>오늘 점심 메뉴 추천 받습니다. 뭐가 좋을까요?</p>
<div class="mt-2 flex justify-between text-gray-500">
<button>좋아요</button>
<button>댓글</button>
<button>공유</button>
</div>
</div>

<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">1시간 전</p>
</div>
</div>
<p>주말에 등산 가실 분 계신가요? 함께 가요!</p>
<div class="mt-2 flex justify-between text-gray-500">
<button>좋아요</button>
<button>댓글</button>
<button>공유</button>
</div>
</div>

<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">2시간 전</p>
</div>
</div>
<p>새로 나온 영화 재미있대요. 같이 보러 갈 사람?</p>
<div class="mt-2 flex justify-between text-gray-500">
<button>좋아요</button>
<button>댓글</button>
<button>공유</button>
</div>
</div>
</div>
</main>

<footer class="bg-gray-200 p-4 text-center">
<p>&copy; 2024 항해플러스. All rights reserved.</p>
</footer>
</div>
</div>
`;

const ErrorPage = () => `
<main class="bg-gray-100 flex items-center justify-center min-h-screen">
<div class="bg-white p-8 rounded-lg shadow-md w-full text-center" style="max-width: 480px">
<h1 class="text-2xl font-bold text-blue-600 mb-4">항해플러스</h1>
<p class="text-4xl font-bold text-gray-800 mb-4">404</p>
<p class="text-xl text-gray-600 mb-8">페이지를 찾을 수 없습니다</p>
<p class="text-gray-600 mb-8">
요청하신 페이지가 존재하지 않거나 이동되었을 수 있습니다.
</p>
<a href="/" class="bg-blue-600 text-white px-4 py-2 rounded font-bold">
홈으로 돌아가기
</a>
</div>
</main>
`;

const LoginPage = () => `
<main class="bg-gray-100 flex items-center justify-center min-h-screen">
<div class="bg-white p-8 rounded-lg shadow-md w-full max-w-md">
<h1 class="text-2xl font-bold text-center text-blue-600 mb-8">항해플러스</h1>
<form>
<div class="mb-4">
<input type="text" placeholder="이메일 또는 전화번호" class="w-full p-2 border rounded">
</div>
<div class="mb-6">
<input type="password" placeholder="비밀번호" class="w-full p-2 border rounded">
</div>
<button type="submit" class="w-full bg-blue-600 text-white p-2 rounded font-bold">로그인</button>
</form>
<div class="mt-4 text-center">
<a href="#" class="text-blue-600 text-sm">비밀번호를 잊으셨나요?</a>
</div>
<hr class="my-6">
<div class="text-center">
<button class="bg-green-500 text-white px-4 py-2 rounded font-bold">새 계정 만들기</button>
</div>
</div>
</main>
`;

const ProfilePage = () => `
<div id="root">
<div class="bg-gray-100 min-h-screen flex justify-center">
<div class="max-w-md w-full">
<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 class="flex justify-around">
<li><a href="/" class="text-gray-600">홈</a></li>
<li><a href="/profile" class="text-blue-600">프로필</a></li>
<li><a href="#" class="text-gray-600">로그아웃</a></li>
</ul>
</nav>

<main class="p-4">
<div class="bg-white p-8 rounded-lg shadow-md">
<h2 class="text-2xl font-bold text-center text-blue-600 mb-8">
내 프로필
</h2>
<form>
<div class="mb-4">
<label
for="username"
class="block text-gray-700 text-sm font-bold mb-2"
>사용자 이름</label
>
<input
type="text"
id="username"
name="username"
value="홍길동"
class="w-full p-2 border rounded"
/>
</div>
<div class="mb-4">
<label
for="email"
class="block text-gray-700 text-sm font-bold mb-2"
>이메일</label
>
<input
type="email"
id="email"
name="email"
value="[email protected]"
class="w-full p-2 border rounded"
/>
</div>
<div class="mb-6">
<label
for="bio"
class="block text-gray-700 text-sm font-bold mb-2"
>자기소개</label
>
<textarea
id="bio"
name="bio"
rows="4"
class="w-full p-2 border rounded"
>
안녕하세요, 항해플러스에서 열심히 공부하고 있는 홍길동입니다.</textarea
>
</div>
<button
type="submit"
class="w-full bg-blue-600 text-white p-2 rounded font-bold"
>
프로필 업데이트
</button>
</form>
</div>
</main>

<footer class="bg-gray-200 p-4 text-center">
<p>&copy; 2024 항해플러스. All rights reserved.</p>
</footer>
</div>
</div>
</div>
`;

document.body.innerHTML = `
${MainPage()}
${ProfilePage()}
${LoginPage()}
${ErrorPage()}
`;
import loginPage from "./pages/loginPage";
import mainPage from "./pages/mainPage";
import profilePage from "./pages/profilePage";
import errorPage from "./pages/errorPage";

//url변경
export function navigateTo(url) {
window.history.pushState(null, null, url);
renderPage();
}

//페이지 렌더링
function renderPage() {
const route = window.location.pathname;
const rootDiv = document.getElementById("root"); // root div 가져오기
let component;
switch (route) {
case "/":
component = mainPage();
// rootDiv.innerHTML = MainPage(); // MainPage 렌더링
break;
case "/login":
component = loginPage();
break;
case "/profile":
if (!chkLogin()) {
return;
}
component = profilePage(); // ProfilePage 렌더링
break;
default:
component = errorPage();
}
rootDiv.innerHTML = ""; // 기존 내용을 비우기
rootDiv.appendChild(component); // LoginPage 추가
}

function chkLogin() {
if (!window.localStorage.getItem("user")) {
// 로그인이 되지 않았다면
navigateTo("/login");
return false;
} else {
return true;
}
}

window.addEventListener("popstate", renderPage);
window.addEventListener("load", renderPage);
Loading
Loading