Skip to content

Commit

Permalink
refactor: Observer패턴 구현 및 디렉터리 구조 및 관심사 분리 등 개선
Browse files Browse the repository at this point in the history
  • Loading branch information
lshyun955 committed Dec 22, 2024
1 parent 4e0f067 commit 01134c6
Show file tree
Hide file tree
Showing 34 changed files with 461 additions and 278 deletions.
8 changes: 8 additions & 0 deletions src/__tests__/basic.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,16 @@ describe("기본과제 테스트", () => {

describe("6. 기본적인 에러 처리", () => {
it("잘못된 라우트 접근 시 404 페이지로 리다이렉션된다", () => {
console.log("before", document.body.innerHTML);
console.log(
"======================== end before ========================",
);
window.history.pushState({}, "", "/nonexistent");
window.dispatchEvent(new Event("popstate"));
console.log("after", document.body.innerHTML);
console.log(
"======================== end after ========================",
);
expect(document.body.innerHTML).toContain("404");
expect(document.body.innerHTML).toContain("페이지를 찾을 수 없습니다");
});
Expand Down
2 changes: 1 addition & 1 deletion src/components/Footer.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export default function Footer() {
export function Footer() {
return `
<footer class="bg-gray-200 p-4 text-center">
<p>&copy; 2024 항해플러스. All rights reserved.</p>
Expand Down
43 changes: 42 additions & 1 deletion src/components/Header.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,48 @@
export default function Header() {
import { user, globalStore } from "@/store";
import router from "@/router";
import { addEvent } from "@/utils";

const getItemClass = (pathName) => {
const path = router.get().path;

return pathName === path ? "text-blue-600 font-bold" : "text-gray-600";
};

const Nav = ({ isLogin }) => {
if (!isLogin) {
return `<li><a href="/login" class="${getItemClass("/login")}" data-link>로그인</a></li>`;
}

return `
<li><a href="/profile" class="${getItemClass("/profile")}" data-link>프로필</a></li>
<li><a href="#" id="logout" class="text-gray-600">로그아웃</a></li>
`;
};

function logout({ username }) {
globalStore.setState({ currentUser: null, isLogin: false });
router.get().push("/login");
user.remove({ username });
}

export function Header({ isLogin }) {
return `
<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="${getItemClass("/")}" data-link>홈</a></li>
${Nav({ isLogin })}
</ul>
</nav>
`;
}

addEvent("click", "#logout", (e) => {
const { currentUser } = globalStore.getState();

e.preventDefault();
logout({ username: currentUser.username });
});
21 changes: 0 additions & 21 deletions src/components/Nav.js

This file was deleted.

3 changes: 3 additions & 0 deletions src/components/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./Footer";
export * from "./Header";
export * from "./posts";
17 changes: 17 additions & 0 deletions src/components/posts/Post.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export const Post = ({ author, time, content, id }) => `
<div class="bg-white rounded-lg shadow p-4 mb-4">
<div class="flex items-center mb-2">
<img src="https://via.placeholder.com/40" alt="프로필" class="rounded-full mr-2">
<div>
<div class="font-bold">${author}</div>
<div class="text-gray-500 text-sm">${time}</div>
</div>
</div>
<p>${content}</p>
<div class="mt-2 flex justify-between text-gray-500">
<span class="like-button" data-post-id="${id}">좋아요</span>
<span>댓글</span>
<span>공유</span>
</div>
</div>
`;
6 changes: 6 additions & 0 deletions src/components/posts/PostForm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const PostForm = () => `
<div class="mb-4 bg-white rounded-lg shadow p-4">
<textarea id="post-content" placeholder="무슨 생각을 하고 계신가요?" class="w-full p-2 border rounded"></textarea>
<button id="post-submit" class="mt-2 bg-blue-600 text-white px-4 py-2 rounded">게시</button>
</div>
`;
2 changes: 2 additions & 0 deletions src/components/posts/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./Post";
export * from "./PostForm";
9 changes: 9 additions & 0 deletions src/error/ForbiddenError.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// 에러 메시지를 상수로 분리
const ERROR_MESSAGE = "ForbiddenError";

// 순수 함수로 에러 객체 생성
export const ForbiddenError = (message = ERROR_MESSAGE) => ({
name: "ForbiddenError",
message,
stack: new Error().stack,
});
9 changes: 9 additions & 0 deletions src/error/NotFoundError.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// 에러 메시지를 상수로 분리
const ERROR_MESSAGE = "NotFoundError";

// 순수 함수로 에러 객체 생성
export const NotFoundError = (message = ERROR_MESSAGE) => ({
name: "NotFoundError",
message,
stack: new Error().stack,
});
9 changes: 9 additions & 0 deletions src/error/UnauthorizedError.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// 에러 메시지를 상수로 분리
const ERROR_MESSAGE = "UnauthorizedError";

// 순수 함수로 에러 객체 생성
export const UnauthorizedError = (message = ERROR_MESSAGE) => ({
name: "UnauthorizedError",
message,
stack: new Error().stack,
});
3 changes: 3 additions & 0 deletions src/error/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./ForbiddenError";
export * from "./NotFoundError";
export * from "./UnauthorizedError";
34 changes: 34 additions & 0 deletions src/lib/createHashRouter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { createObserver } from "./createObserver";

export function createHashRouter(routes) {
const { subscribe, notify } = createObserver();

const getPath = () =>
window.location.hash ? window.location.hash.slice(1) : "/";

const getTarget = () => routes[getPath()];

const push = (path) => {
console.log(path);
window.location.hash = path;
};

// 해시 변경 이벤트 리스너
window.addEventListener("hashchange", notify);

// 초기 로드 시 해시 없는 경우 처리
window.addEventListener("load", () => {
if (!window.location.hash) {
push("/");
}
});

return {
get path() {
return getPath();
},
push,
getTarget,
subscribe,
};
}
7 changes: 7 additions & 0 deletions src/lib/createObserver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const createObserver = () => {
const listeners = new Set();
const subscribe = (fn) => listeners.add(fn);
const notify = () => listeners.forEach((listener) => listener());

return { subscribe, notify };
};
25 changes: 25 additions & 0 deletions src/lib/createRouter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { createObserver } from "./createObserver";

export function createRouter(routes) {
const { subscribe, notify } = createObserver();

const getPath = () => window.location.pathname;

const getTarget = () => routes[getPath()];

const push = (path) => {
window.history.pushState(null, null, path);
notify();
};

window.addEventListener("popstate", () => notify());

return {
get path() {
return getPath();
},
push,
getTarget,
subscribe,
};
}
16 changes: 16 additions & 0 deletions src/lib/createStore.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { createObserver } from "./createObserver";

export const createStore = (initialStore) => {
const { subscribe, notify } = createObserver();

let state = { ...initialStore };

const setState = (newState) => {
state = { ...state, ...newState };
notify();
};

const getState = () => ({ ...state });

return { getState, setState, subscribe };
};
File renamed without changes.
4 changes: 4 additions & 0 deletions src/lib/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "./createRouter";
export * from "./createHashRouter";
export * from "./hooks";
export * from "./createStore";
25 changes: 24 additions & 1 deletion src/main.hash.js
Original file line number Diff line number Diff line change
@@ -1 +1,24 @@
import "./main.js";
import { globalStore } from "@/store";
import { AuthGuard } from "@/utils";
import { ForbiddenError, UnauthorizedError } from "@/error";
import router from "@/router";
import { render } from "@/render";
import { createHashRouter } from "@/lib";
import { MainPage, LoginPage, ProfilePage } from "@/pages";

router.set(
createHashRouter({
"/": MainPage,
"/login": AuthGuard(Boolean, ForbiddenError, LoginPage),
"/profile": AuthGuard((value) => !value, UnauthorizedError, ProfilePage),
}),
);

function mainHash() {
router.get().subscribe(render);
globalStore.subscribe(render);

render();
}

mainHash();
106 changes: 22 additions & 84 deletions src/main.js
Original file line number Diff line number Diff line change
@@ -1,86 +1,24 @@
import User from "./data/user";
import createRouter from "./routes";

const [router, hasRouter] = createRouter();

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

if (tagName !== "A") {
return;
}

e.preventDefault();

let path = href.split(window.location.origin)[1];

if (id === "logout") {
// 유저 정보 전역 상태 관리
User().set({ username: "", isLogout: true });
path = "/login";
}

router(path);
}

function submitEvent(e) {
e.preventDefault();

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

switch (id) {
case "login-form":
login(formData);
break;
case "profile-form":
updateProfile(formData);
break;
}
import { createRouter } from "@/lib";
import { MainPage, LoginPage, ProfilePage } from "@/pages";
import { globalStore } from "@/store";
import { ForbiddenError, UnauthorizedError } from "@/error";
import router from "@/router";
import { render } from "@/render";
import { AuthGuard } from "@/utils";

router.set(
createRouter({
"/": MainPage,
"/login": AuthGuard(Boolean, ForbiddenError, LoginPage),
"/profile": AuthGuard((value) => !value, UnauthorizedError, ProfilePage),
}),
);

function main() {
router.get().subscribe(render);
globalStore.subscribe(render);

render();
}

function updateProfile(formData) {
const user = {
username: "",
email: "",
bio: "",
};

formData.forEach((value, key) => {
user[key] = value;
});

User().set(user);

// 유저 로그인 정보 전역상태 관리 필요
router("/profile");
}

function login(formData) {
const username = formData.get("username");

if (!username) {
return;
}

const user = {
username,
email: "",
bio: "",
};

// 유저 로그인 정보 전역상태 관리 필요
User().set(user);

router("/profile");
}

document.body.addEventListener("click", clickEvent);
document.body.addEventListener("submit", submitEvent);

window.addEventListener("load", () => router());
window.addEventListener("hashchange", () => hasRouter());
window.addEventListener("popstate", (e) => {
router(e.target.location.pathname);
});
main();
2 changes: 1 addition & 1 deletion src/pages/ErrorPage.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export default function ErrorPage() {
export function ErrorPage() {
return `
<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">
Expand Down
Loading

0 comments on commit 01134c6

Please sign in to comment.