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 만들기 #15

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
37 changes: 7 additions & 30 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"devDependencies": {
"@eslint/js": "^9.16.0",
"@playwright/test": "^1.49.1",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/user-event": "^14.5.2",
"@vitest/ui": "^2.1.8",
Expand Down
2 changes: 2 additions & 0 deletions src/app/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as router } from "./router";
export { default as routes } from "./routes";
53 changes: 53 additions & 0 deletions src/app/router.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import Routes from "./routes";
import userService from "../features/UserService";
import { Store } from "../features";

const store = Store.getInstance();

export const historyRouter = (path) => {
store.clearListeners();
Copy link

Choose a reason for hiding this comment

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

라우터에 클린업 함수를 추가하셨네요! 이렇게 하면 불필요한 이벤트 리스너가 남지 않아 성능 개선이 도움이 될 거 같아요. 저도 참고하겠습니다👍🏻

Copy link
Author

Choose a reason for hiding this comment

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

사실 지금으로선 괜찮지만, 라우트를 변경해도 유지되어야 하는 subscriber가 있을 경우를 고려해서 다른 방식을 찾고 싶었는데... 현재 구조에선 찾지 못해서 이렇게 구현했어요ㅎ.ㅎ


const pathToGo = interceptor(path);

history.pushState({}, "", pathToGo);

const page = Routes[pathToGo] ?? Routes[404];
const { view, init } = page();

document.querySelector("#root").innerHTML = view;
init();
};

export const hashRouter = (hash) => {
store.clearListeners();

const path = hash.replace("#", "");
const pathToGo = interceptor(path);

window.location.hash = pathToGo;

const page = Routes[pathToGo] ?? Routes[404];
const { view, init } = page();

document.querySelector("#root").innerHTML = view;
init();
};

const interceptor = (path) => {
let redirectedPath;

if (path === "/profile" && !userService.isLoggedIn()) {
redirectedPath = "/login";
}

if (path === "/login" && userService.isLoggedIn()) {
redirectedPath = "/";
}

return redirectedPath ?? path;
};

const router = (path) =>
window.location.hash ? hashRouter(path) : historyRouter(path);

export default router;
10 changes: 10 additions & 0 deletions src/app/routes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ErrorPage, LoginPage, MainPage, ProfilePage } from "../pages";

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

export default Routes;
39 changes: 39 additions & 0 deletions src/features/Store.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
const Store = (function () {
let instance;

const createStore = (initState = {}) => {
let state = initState;
const listeners = new Set();

return {
getState: () => state,

setState: (updater) => {
state = typeof updater === "function" ? updater(state) : updater;
listeners.forEach((listener) => {
listener(state);
});
},

subscribe: (listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
},

clearListeners: () => {
listeners.clear();
},
};
};

return {
getInstance: (initState = {}) => {
if (!instance) {
instance = createStore(initState);
}
return instance;
},
};
})();

export default Store;
27 changes: 27 additions & 0 deletions src/features/UserService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { USER_KEY } from "../shared/const";

class UserService {
isLoggedIn = () => !!this.getUser();

setUser = ({ username, email, bio }) => {
if (email && !this.isValidEmail(email)) {
alert("이메일 형식을 확인해주세요.");
}
localStorage.setItem(USER_KEY, JSON.stringify({ username, email, bio }));
};

getUser = () => {
return JSON.parse(localStorage.getItem(USER_KEY));
};

clearUser = () => {
return localStorage.removeItem(USER_KEY);
};

emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
isValidEmail = (val) => this.emailRegex.test(val);
}

const userService = new UserService();

export default userService;
2 changes: 2 additions & 0 deletions src/features/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as UserService } from "./UserService";
export { default as Store } from "./Store";
33 changes: 33 additions & 0 deletions src/initApp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { router } from "./app";
import { Store } from "./features";
import { POST_LIST } from "./shared";

const initLoadedListener = () => {
document.addEventListener("DOMContentLoaded", () => {
const currentPath = window.location.hash || window.location.pathname;
router(currentPath);
});
};

const initPopListener = () => {
window.addEventListener("popstate", () => {
const currentPath = window.location.hash || window.location.pathname;
router(currentPath);
});
};

const initHashChangeListener = () => {
window.addEventListener("hashchange", () => {
const currentPath = window.location.hash || window.location.pathname;
router(currentPath);
});
};

export const initApp = () => {
initPopListener();
initHashChangeListener();
initLoadedListener();

const store = Store.getInstance();
store.setState({ postList: POST_LIST });
};
Loading
Loading