-
Notifications
You must be signed in to change notification settings - Fork 65
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-2] 프레임워크 없이 SPA 만들기 #4
base: main
Are you sure you want to change the base?
Changes from 10 commits
2294aad
091f8e3
21b6978
6a2f991
520f730
3d4ff2e
d295a98
9d6b5f4
3c7fc7d
731a819
8e308f5
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 |
---|---|---|
@@ -1,20 +1,37 @@ | ||
/** @jsx createVNode */ | ||
import { createVNode } from "../../lib"; | ||
import { globalStore } from "../../stores/index.js"; | ||
|
||
export const PostForm = () => { | ||
const { addPost } = globalStore.actions; | ||
|
||
const handlePostSubmit = (e) => { | ||
e.preventDefault(); | ||
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. e.preventDefault() 을 추가해주셔서 제출 동작을 막아주시고 계시는 군요!! 잘 추가해주신 것 같습니다 bb |
||
const contentInput = document.getElementById("post-content"); | ||
const content = contentInput.value.trim(); | ||
if (!content) return; | ||
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.
|
||
|
||
addPost(content); | ||
contentInput.value = ""; | ||
}; | ||
return ( | ||
<div className="mb-4 bg-white rounded-lg shadow p-4"> | ||
<form | ||
onSubmit={handlePostSubmit} | ||
className="mb-4 bg-white rounded-lg shadow p-4" | ||
> | ||
<textarea | ||
id="post-content" | ||
name="content" | ||
placeholder="무슨 생각을 하고 계신가요?" | ||
className="w-full p-2 border rounded" | ||
/> | ||
<button | ||
id="post-submit" | ||
type="submit" | ||
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.
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. 아 그렇네요 이걸 까먹고 있었는데 감사합니다 ㅎㅎ |
||
className="mt-2 bg-blue-600 text-white px-4 py-2 rounded" | ||
> | ||
게시 | ||
</button> | ||
</div> | ||
</form> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,53 @@ | ||
import { addEvent } from "./eventManager"; | ||
import { addEvent } from "./eventManager.js"; | ||
import { supportedEventNames } from "./extractEvent.js"; | ||
|
||
export function createElement(vNode) {} | ||
/** | ||
* DOM API를 이용하여 Virtual DOM을 실제 DOM으로 변환한다. | ||
*/ | ||
export function createElement(vNode) { | ||
// 문자열이나 숫자 처리 | ||
if (typeof vNode === "string" || typeof vNode === "number") { | ||
return document.createTextNode(String(vNode)); | ||
} | ||
|
||
function updateAttributes($el, props) {} | ||
// null, undefined, boolean 처리 | ||
if (vNode == null || typeof vNode === "boolean") { | ||
return document.createTextNode(""); | ||
} | ||
|
||
// 배열(fragment) 처리 | ||
if (Array.isArray(vNode)) { | ||
const fragment = document.createDocumentFragment(); | ||
vNode.forEach((child) => { | ||
const childElement = createElement(child); | ||
fragment.appendChild(childElement); | ||
}); | ||
return fragment; | ||
} | ||
|
||
const element = document.createElement(vNode.type); | ||
|
||
// props 처리 | ||
if (vNode.props) { | ||
Object.entries(vNode.props).forEach(([key, value]) => { | ||
if (key === "className") { | ||
element.setAttribute("class", value); | ||
} else if (key.startsWith("on") && typeof value === "function") { | ||
const eventType = key.slice(2).toLowerCase(); | ||
if (supportedEventNames.has(eventType)) { | ||
addEvent(element, eventType, value); | ||
} | ||
} else if (key !== "key" && key !== "children") { | ||
element.setAttribute(key, value); | ||
} | ||
}); | ||
} | ||
|
||
// 자식 노드 처리 | ||
vNode.children.forEach((child) => { | ||
const childElement = createElement(child); | ||
element.appendChild(childElement); | ||
}); | ||
|
||
return element; | ||
} |
Original file line number | Diff line number | Diff line change | |||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,138 @@ | |||||||||||||||||||||||
/** | |||||||||||||||||||||||
* 합성 이벤트 생성자 함수를 반환하는 팩토리 함수 | |||||||||||||||||||||||
* @param Interface Interface 이벤트 타입 별 속성 정의 | |||||||||||||||||||||||
* @returns {function(*, *, *, *): SyntheticBaseEvent} 합성 이벤트 생성자 함수 | |||||||||||||||||||||||
*/ | |||||||||||||||||||||||
function createSyntheticEvent(Interface) { | |||||||||||||||||||||||
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. 와 이거 코드 내일 설명해 주시면 안될까요?! 클래스가 아닌 함수로 구현하신 이유도 궁금합니다. 패턴 명도 궁금하구요! 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. 발제시간에 얘기해보도록 하겠습니다 ㅎㅎ |
|||||||||||||||||||||||
// 기본 합성 이벤트 생성자 | |||||||||||||||||||||||
function SyntheticBaseEvent(name, eventType, nativeEvent, nativeEventTarget) { | |||||||||||||||||||||||
this._name = name; | |||||||||||||||||||||||
this.nativeEvent = nativeEvent; | |||||||||||||||||||||||
this.target = nativeEventTarget; | |||||||||||||||||||||||
this.currentTarget = null; | |||||||||||||||||||||||
|
|||||||||||||||||||||||
// Interface에 정의된 속성들을 인스턴스에 복사 | |||||||||||||||||||||||
for (let prop in Interface) { | |||||||||||||||||||||||
if (Object.hasOwn(Interface, prop)) { | |||||||||||||||||||||||
this[prop] = Interface[prop]; | |||||||||||||||||||||||
} | |||||||||||||||||||||||
} | |||||||||||||||||||||||
Comment on lines
+15
to
+19
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. 오호 여기서 혹시 모든 속성을 반복해서 복사하는 방식으로 구현되고 있을까용? 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. 제가 이해하기론 for (let prop in Interface) {
this[prop] = Interface[prop];
} 예를 들어 위와 같이 할 경우 toString, valueOf와 같은 프로토타입 체인에 있는 모든 속성들까지 복사되기 때문에 |
|||||||||||||||||||||||
|
|||||||||||||||||||||||
// 이벤트 취소/전파 중단 상태 초기화 | |||||||||||||||||||||||
const defaultPrevented = nativeEvent.defaultPrevented ?? false; | |||||||||||||||||||||||
this.isDefaultPrevented = () => defaultPrevented; | |||||||||||||||||||||||
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. 혹시 이 메서드를 통해서 어떤 역할을 수행하는지 여쭤봐도될까요? 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.
정리하면,
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. 그럼 하위에서 기본 이벤트 동작을 취소하면 버블링되면서 해당 이벤트들의 기본동작들을 모두 취소하는건가요?? |
|||||||||||||||||||||||
this.isPropagationStopped = () => false; | |||||||||||||||||||||||
|
|||||||||||||||||||||||
return this; | |||||||||||||||||||||||
} | |||||||||||||||||||||||
|
|||||||||||||||||||||||
// 이벤트 제어(전파 중단 등)를 위한 메서드 정의 | |||||||||||||||||||||||
Object.assign(SyntheticBaseEvent.prototype, { | |||||||||||||||||||||||
preventDefault: function () { | |||||||||||||||||||||||
this.defaultPrevented = true; | |||||||||||||||||||||||
const event = this.nativeEvent; | |||||||||||||||||||||||
if (!event) return; | |||||||||||||||||||||||
if (event.preventDefault) { | |||||||||||||||||||||||
event.preventDefault(); | |||||||||||||||||||||||
} | |||||||||||||||||||||||
}, | |||||||||||||||||||||||
stopPropagation: function () { | |||||||||||||||||||||||
const event = this.nativeEvent; | |||||||||||||||||||||||
if (!event) return; | |||||||||||||||||||||||
if (event.stopPropagation) { | |||||||||||||||||||||||
event.stopPropagation(); | |||||||||||||||||||||||
} | |||||||||||||||||||||||
this.isPropagationStopped = () => true; | |||||||||||||||||||||||
}, | |||||||||||||||||||||||
}); | |||||||||||||||||||||||
|
|||||||||||||||||||||||
return SyntheticBaseEvent; | |||||||||||||||||||||||
} | |||||||||||||||||||||||
|
|||||||||||||||||||||||
// 기본 이벤트 인터페이스 정의 | |||||||||||||||||||||||
const EventInterface = { | |||||||||||||||||||||||
eventPhase: 0, | |||||||||||||||||||||||
bubbles: 0, | |||||||||||||||||||||||
cancelable: 0, | |||||||||||||||||||||||
defaultPrevented: 0, | |||||||||||||||||||||||
}; | |||||||||||||||||||||||
|
|||||||||||||||||||||||
// UI 이벤트 (click, focus 등의 기본이 되는 이벤트) | |||||||||||||||||||||||
const UIEventInterface = { | |||||||||||||||||||||||
...EventInterface, | |||||||||||||||||||||||
view: 0, | |||||||||||||||||||||||
detail: 0, | |||||||||||||||||||||||
}; | |||||||||||||||||||||||
|
|||||||||||||||||||||||
// 터치 이벤트 | |||||||||||||||||||||||
const TouchEventInterface = { | |||||||||||||||||||||||
...UIEventInterface, | |||||||||||||||||||||||
touches: 0, | |||||||||||||||||||||||
targetTouches: 0, | |||||||||||||||||||||||
changedTouches: 0, | |||||||||||||||||||||||
altKey: 0, | |||||||||||||||||||||||
metaKey: 0, | |||||||||||||||||||||||
ctrlKey: 0, | |||||||||||||||||||||||
shiftKey: 0, | |||||||||||||||||||||||
}; | |||||||||||||||||||||||
|
|||||||||||||||||||||||
// 마우스 이벤트 (click, hover 등) | |||||||||||||||||||||||
const MouseEventInterface = { | |||||||||||||||||||||||
...UIEventInterface, | |||||||||||||||||||||||
screenX: 0, | |||||||||||||||||||||||
screenY: 0, | |||||||||||||||||||||||
clientX: 0, | |||||||||||||||||||||||
clientY: 0, | |||||||||||||||||||||||
pageX: 0, | |||||||||||||||||||||||
pageY: 0, | |||||||||||||||||||||||
ctrlKey: 0, | |||||||||||||||||||||||
shiftKey: 0, | |||||||||||||||||||||||
altKey: 0, | |||||||||||||||||||||||
metaKey: 0, | |||||||||||||||||||||||
button: 0, | |||||||||||||||||||||||
buttons: 0, | |||||||||||||||||||||||
}; | |||||||||||||||||||||||
|
|||||||||||||||||||||||
// 드래그 이벤트 | |||||||||||||||||||||||
const DragEventInterface = { | |||||||||||||||||||||||
...MouseEventInterface, | |||||||||||||||||||||||
dataTransfer: 0, | |||||||||||||||||||||||
}; | |||||||||||||||||||||||
|
|||||||||||||||||||||||
// 터치 이벤트 | |||||||||||||||||||||||
const FocusEventInterface = { | |||||||||||||||||||||||
...UIEventInterface, | |||||||||||||||||||||||
relatedTarget: 0, | |||||||||||||||||||||||
}; | |||||||||||||||||||||||
|
|||||||||||||||||||||||
// 휠 이벤트 (마우스 휠, 트랙 패드) | |||||||||||||||||||||||
const WheelEventInterface = { | |||||||||||||||||||||||
...MouseEventInterface, | |||||||||||||||||||||||
deltaX(event) { | |||||||||||||||||||||||
return "deltaX" in event | |||||||||||||||||||||||
? event.deltaX | |||||||||||||||||||||||
: "wheelDeltaX" in event | |||||||||||||||||||||||
? -event.wheelDeltaX | |||||||||||||||||||||||
: 0; | |||||||||||||||||||||||
}, | |||||||||||||||||||||||
deltaY(event) { | |||||||||||||||||||||||
return "deltaY" in event | |||||||||||||||||||||||
? event.deltaY | |||||||||||||||||||||||
: "wheelDeltaY" in event | |||||||||||||||||||||||
? -event.wheelDeltaY | |||||||||||||||||||||||
: "wheelDelta" in event | |||||||||||||||||||||||
? -event.wheelDelta | |||||||||||||||||||||||
: 0; | |||||||||||||||||||||||
}, | |||||||||||||||||||||||
deltaZ: 0, | |||||||||||||||||||||||
deltaMode: 0, | |||||||||||||||||||||||
}; | |||||||||||||||||||||||
|
|||||||||||||||||||||||
// 각 이벤트 타입별 합성 이벤트 생성자 export | |||||||||||||||||||||||
export const SyntheticEvent = createSyntheticEvent(EventInterface); | |||||||||||||||||||||||
export const SyntheticUIEvent = createSyntheticEvent(UIEventInterface); | |||||||||||||||||||||||
export const SyntheticTouchEvent = createSyntheticEvent(TouchEventInterface); | |||||||||||||||||||||||
export const SyntheticMouseEvent = createSyntheticEvent(MouseEventInterface); | |||||||||||||||||||||||
export const SyntheticDragEvent = createSyntheticEvent(DragEventInterface); | |||||||||||||||||||||||
export const SyntheticFocusEvent = createSyntheticEvent(FocusEventInterface); | |||||||||||||||||||||||
export const SyntheticWheelEvent = createSyntheticEvent(WheelEventInterface); | |||||||||||||||||||||||
Comment on lines
+1
to
+138
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. 와우 이걸 다 정의하셨군요.. bb!
Comment on lines
+132
to
+138
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. 와,,, 이런 다양한 이벤트들 정의,,하시고 그냥 최곱니다,,, bb 이걸 사용하는 쪽 코드가 궁금해지네요 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. 사실 간단하게 작동하는지 테스트는 해보았지만 직접 적용해보진 못했습니다 ㅠㅠ 현재는 click과 관련한 MouseEvent만 주로 활용되고 있어요 시간이 만약 더 있었다면 wheel 이벤트 등을 활용해서 무한 pagination + throttling을 적용해보고싶다는 생각만 해보았습니다 😂
Comment on lines
+132
to
+138
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. 도운님 이벤트를 각 특징의 표로 정리해봤어요~! 리뷰하시는 분들 참고해주세요!
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,13 @@ | ||
/** | ||
* Virtual DOM 노드를 생성 | ||
* 자식 요소는 모두 평탄화하며, 유효하지 않은 자식 요소는 제거 | ||
*/ | ||
export function createVNode(type, props, ...children) { | ||
return {}; | ||
return { | ||
type, | ||
props: props || null, | ||
children: children | ||
.flat(Infinity) | ||
.filter((child) => child != null && child !== false && child !== true), | ||
}; | ||
} |
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.
globalStore
에서currentUser
를 받아오는 거라면loggedIn
또한 받아 올 수 있을 것 같은데loggedIn
은 컴포넌트 인자로 받으신 이유가 있을까요?비슷한 이유로
toggleLike
도globalStore
에서 관리하면 로그인 검사 로직을 안으로 넣을 수 있을 것 같은데 로그인 검사는 관심사가 달라서 일부러 분리하신 건지 궁금합니다!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.
loggedIn
을 Home에서 먼저 사용하다보니 다시 선언하기보단 재사용하기 위해Post
컴포넌트에 props로 넘겨주는게 더 효율적이라고 생각했어요. 근데 또 다시 생각해보면 React에서 Provider 이용해서 합성 컴포넌트 만들듯이 PostPage에서 선언하고 분리하는게 더 깔끔할 수 있다는 생각이 드네요..!alert를 화면에 보여주는게 UI에 관여하는 작업이라고 생각이 들어서 비즈니스 로직과 분리하기 위해 의도적으로 이렇게 작성했습니다!