-
Notifications
You must be signed in to change notification settings - Fork 77
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
[8팀 김도운] [Chapter 1-1] 프레임워크 없이 SPA 만들기 #22
base: main
Are you sure you want to change the base?
Changes from all commits
34b4355
4a6040d
67815e3
c5d9493
c379efd
a68d83b
9f49657
1564f43
76b07b8
4210f37
1813a8f
69ead67
c245e3a
eaf8f1c
3752654
e7c41e2
57de2d9
2b83adb
234149e
4220bde
7a34c49
262e41e
8e1658e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{ | ||
"compilerOptions": { | ||
"baseUrl": ".", | ||
"paths": { | ||
"@/*": ["./src/*"] | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import NavbarController from "../controllers/NavbarController"; | ||
|
||
class ComponentBinding { | ||
$target; | ||
|
||
constructor($target) { | ||
this.$target = $target; | ||
this._dependencies(); | ||
} | ||
_dependencies() { | ||
new NavbarController(this.$target); | ||
} | ||
} | ||
|
||
export default ComponentBinding; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import LoginController from "../controllers/LoginController"; | ||
import ProfileController from "../controllers/ProfileController"; | ||
import Binding from "../core/Binding"; | ||
|
||
class PageBinding extends Binding { | ||
_dependencies() { | ||
new ProfileController(this.$target); | ||
new LoginController(this.$target); | ||
} | ||
} | ||
|
||
export default PageBinding; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import ComponentBinding from "./ComponentBinding"; | ||
import PageBinding from "./PageBinding"; | ||
|
||
const createBindings = ($target) => { | ||
const componentBinding = new ComponentBinding($target); | ||
const pageBinding = new PageBinding($target); | ||
|
||
return { | ||
component: componentBinding, | ||
page: pageBinding, | ||
}; | ||
}; | ||
|
||
export default createBindings; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import Component from "../core/Component"; | ||
|
||
class Navbar extends Component { | ||
template() { | ||
return ` | ||
<nav class="navbar bg-white shadow-md p-2 sticky top-14"> | ||
<ul class="flex justify-around"> | ||
<li><a href="/" class="nav-link ${this.controller.isMainPage() ? "text-blue-600 font-bold" : "text-gray-600"}">홈</a></li> | ||
${ | ||
this.controller.auth | ||
? `<li><a href="/profile" class="nav-link ${!this.controller.isMainPage() ? "text-blue-600 font-bold" : "text-gray-600"}">프로필</a></li> | ||
<li><a href="/login" id="logout" class="nav-link text-gray-600">로그아웃</a></li>` | ||
: `<li><a href="/login" id="login" class="nav-link text-gray-600">로그인</a></li>` | ||
} | ||
</ul> | ||
</nav> | ||
`; | ||
} | ||
} | ||
|
||
export default Navbar; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import UserService from "../service/UserService.js"; | ||
import Controller from "../core/Controller"; | ||
import ProfilePage from "../pages/Profile"; | ||
import Router from "../router/Router"; | ||
import ProfileController from "./ProfileController"; | ||
|
||
class LoginController extends Controller { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 각각의 역할별로 컨트롤러를 분리하시고 core의 컨트롤러를 가지고 확장하는 구조를 가지고 있군요 ... 헙🧐 도대체 얼마나 많은 고민과 노력을 하셨을지 ... 👍 도운님의 코드의 의도를 잘 파악해 보겠습니다 각각의 관심사별로 잘 분리해 사용하고 계신 것 같습니다 bb 최고최고 |
||
attachListeners() { | ||
// 로그인 폼 제출 시 로그인 처리 및 프로필 페이지 초기화 | ||
this.addListener("submit", "#login-form", (e) => { | ||
e.preventDefault(); | ||
const username = this.$target.querySelector(".username").value.trim(); | ||
|
||
if (!username) return; | ||
|
||
UserService.login(username); | ||
|
||
// 새 프로필 컨트롤러 및 페이지 생성, 라우트 등록 | ||
ProfileController.instance.onRefresh(); | ||
const profileInstnace = new ProfilePage( | ||
this.$target, | ||
ProfileController.instance, | ||
); | ||
Router.instance.addRoute("/profile", () => profileInstnace); | ||
Router.instance.navigate("/"); | ||
}); | ||
} | ||
} | ||
|
||
export default LoginController; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import UserService from "../service/UserService"; | ||
import Router from "../router/Router"; | ||
import Controller from "../core/Controller"; | ||
|
||
class NavbarController extends Controller { | ||
attachListeners() { | ||
// 네비게이션 링크 클릭 시 라우팅 처리 | ||
this.addListener("click", ".nav-link", (e) => { | ||
const target = e.target.closest("a"); | ||
if (!(target instanceof HTMLAnchorElement)) return; | ||
|
||
e.preventDefault(); | ||
|
||
// 로그아웃 버튼 처리 | ||
if (target.id === "logout") { | ||
this.handleLogout(); | ||
} | ||
|
||
// 대상 네비게이션 탭 URL로 이동 | ||
const targetURL = e.target.getAttribute("href"); | ||
Router.instance.navigate(targetURL); | ||
}); | ||
} | ||
|
||
handleLogout() { | ||
UserService.logout(); | ||
} | ||
|
||
// 인증 정보 반환 | ||
get auth() { | ||
return UserService.getAuth(); | ||
} | ||
|
||
// 현재 페이지가 메인 페이지인지 판단 | ||
isMainPage() { | ||
const currentPath = window.location.pathname; | ||
const currentHash = window.location.hash; | ||
return currentPath === "/" || currentHash === "#/"; | ||
} | ||
} | ||
|
||
export default NavbarController; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import UserService from "../service/UserService"; | ||
import Controller from "../core/Controller"; | ||
|
||
class ProfileController extends Controller { | ||
_onInit() { | ||
// 사용자 정보 로드 및 상태 초기화 | ||
const user = UserService.getUser(); | ||
this.state = { | ||
username: user?.username || "", | ||
email: user?.email || "", | ||
bio: user?.bio || "", | ||
}; | ||
super._onInit(); | ||
} | ||
|
||
onRefresh() { | ||
this._onInit(); | ||
} | ||
|
||
attachListeners() { | ||
// 프로필 폼 제출 시 사용자 정보 업데이트 | ||
this.addListener("submit", "#profile-form", (e) => { | ||
e.preventDefault(); | ||
|
||
const form = e.target.closest("form"); | ||
const username = form.querySelector("#username").value; | ||
const email = form.querySelector("#email").value; | ||
const bio = form.querySelector("#bio").value; | ||
|
||
UserService.saveUser(username, email, bio); | ||
|
||
this.setState({ username, email, bio }); | ||
alert("프로필이 업데이트 되었습니다"); | ||
}); | ||
} | ||
} | ||
|
||
export default ProfileController; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
class Binding { | ||
$target; | ||
|
||
constructor($target) { | ||
this.$target = $target; | ||
this._dependencies(); | ||
} | ||
|
||
// 의존성 주입 | ||
_dependencies() {} | ||
} | ||
|
||
export default Binding; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
class Component { | ||
$target; | ||
controller; | ||
|
||
constructor($target, controller) { | ||
this.$target = $target; | ||
this.controller = controller; | ||
|
||
// 컨트롤러가 있으면 상태 변경 시 자동 리렌더링 | ||
if (this.controller) { | ||
this.controller.setOnStateChange(() => this.render()); | ||
} | ||
|
||
this.init(); | ||
Comment on lines
+9
to
+14
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저는 도운님의 코드를 보고 이것을 위해 Core로 Components를 개발해두신 게 아닐까?! 생각했습니다 우선 이 구조를 보고 컴포넌트의 렌더링 로직들이 캡슐화되어있어서 관리하기도 쉽고, 애초에 이 컴포넌트를 사용하고자 하는 도메인에 의존한 컴포넌트에서 상속받아서 쉽게 사용할 수 있다는 것, 그리고 이 코드의 장점인 컨트롤러를 통한 상태 관리 구조는 컴포넌트의 상태 변화에 따른 자동 리렌더링을 지원하게 한다는 것👏🏻👏🏻👏🏻 이 가장 두드러지는 장점인것 같아요! 저 혹시 잘 이해한거 맞나요 ..?ㅎㅎ 하하 코드 리뷰가 아니고 도운님의 멋진 코드를 더 고민해보는 리뷰가 되었네요 이 구조를 처음부터 끝까지 설계하시다니,,, 존경스럽습니다 많이 배우고 갑니다! 고생하셨습니다 도운님 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 웹앱 설계에 익숙하지 않은 패턴이기도 하고 어쩌면 복잡한 구조일 수 있는데 완벽히 이해해주셔서 개발한 사람으로써 매우 감사할 따름입니다..👍 소현님에게 제 코드가 조금이라도 도움이 되었다면 이번 과제는 성공적이네요 !! 소현님 코드 리뷰 보면서 저도 다시 한번 복기할 수 있어 좋은 기회였습니다 😁 |
||
} | ||
|
||
// 초기화 로직용 훅 | ||
init() {} | ||
|
||
// 렌더링할 템플릿 반환 | ||
template() { | ||
return ``; | ||
} | ||
|
||
// DOM 업데이트 | ||
render() { | ||
this.$target.innerHTML = this.template(); | ||
this.mount(); | ||
} | ||
|
||
// 마운트 훅 | ||
mount() {} | ||
} | ||
|
||
export default Component; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
class Controller { | ||
state; | ||
static _instance = null; | ||
|
||
static get instance() { | ||
if (!this._instance) { | ||
throw new Error(`Instance of ${this.name} has not been created yet.`); | ||
} | ||
return this._instance; | ||
} | ||
|
||
constructor($target) { | ||
// 싱글톤 패턴 | ||
if (this.constructor._instance) { | ||
return this.constructor._instance; | ||
} | ||
this.constructor._instance = this; | ||
|
||
this.$target = $target; | ||
this.onStateChange = null; | ||
this.state = {}; | ||
this._onInit(); | ||
} | ||
|
||
// 초기화 시 attachListeners 호출 | ||
_onInit() { | ||
this.attachListeners(); | ||
} | ||
|
||
// 이벤트 리스너 등록 헬퍼 메서드 | ||
addListener(eventType, selector, callback) { | ||
const boundCallback = (event) => { | ||
if (!event.target.closest(selector)) return false; | ||
callback(event); | ||
}; | ||
// 기존 리스너 제거 후 새 리스너 추가 | ||
this.$target.removeEventListener(eventType, boundCallback); | ||
this.$target.addEventListener(eventType, boundCallback); | ||
} | ||
|
||
// 리스너를 등록하는 훅, 하위 클래스에서 구현 | ||
attachListeners() {} | ||
|
||
// 상태 변경 시 onStateChange 콜백 실행 | ||
setState(newState) { | ||
this.state = { ...this.state, ...newState }; | ||
this.onStateChange(); | ||
} | ||
|
||
// 상태 변경 시 호출할 콜백 설정 | ||
setOnStateChange(callback) { | ||
this.onStateChange = callback; | ||
} | ||
|
||
// 컨트롤러 해제 시 인스턴스 제거 | ||
dispose() { | ||
this.constructor.instance = null; | ||
} | ||
} | ||
|
||
export default Controller; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오호 특정 DOM 요소($target)에 대한 바인딩을 수행하는 역할을 컴포넌트와 페이지로 분리하셨군요,,!?!?
혹시 이렇게 구현하게 된 이유가 있으신가요? 요 역할을 따로 분리해주신게 넘 신기해요 👍 새로운 구조를 하나 알아갑니다,,,