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

[10팀 이용] [Chapter 1-2] 프레임워크 없이 SPA 만들기 #30

Open
wants to merge 31 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
6f5aa29
feat: createVNode 함수 구현
nogy21 Dec 22, 2024
c3a9953
feat: createVNode 함수 반환값 중 children 요소 평탄화 추가
nogy21 Dec 22, 2024
fded515
feat: normalizeVNode 값 검증 구현
nogy21 Dec 22, 2024
a71a46a
feat: normalizeVNode 유효성 검증 로직 개선
nogy21 Dec 22, 2024
feb9a34
feat: normalizeVNode 함수에 컴포넌트 정규화 로직 추가
nogy21 Dec 22, 2024
3a894f2
feat: normalizeVNode 함수에서 props 기본값을 null로 변경
nogy21 Dec 22, 2024
0102a2d
refactor: 함수 타입의 vNode 정규화 로직 함수로 추출
nogy21 Dec 22, 2024
8b6b165
refactor: 유효성 검증 및 문자열 확인 함수를 유틸리티 함수로 파일 분리
nogy21 Dec 22, 2024
ee1f183
feat: createElement 함수에 유효성 검증 및 문자열 처리 로직 추가
nogy21 Dec 22, 2024
66569fe
feat: createElement 함수에 배열 처리 로직 추가
nogy21 Dec 22, 2024
13766b0
refactor: createElement 함수에서 배열 처리 로직을 createFragmentWithArray 함수로 분리
nogy21 Dec 22, 2024
eaa99f1
feat: createElement 함수에 props 및 children 처리 로직 추가
nogy21 Dec 23, 2024
b70cea1
feat: eventManager 이벤트 위임 등록
nogy21 Dec 23, 2024
fd03ada
feat: eventManager에서 이벤트 리스너 배열로 변경 및 제거 기능 추가
nogy21 Dec 23, 2024
50b28d8
feat: renderElement 이벤트 등록 구현
nogy21 Dec 23, 2024
3e489bc
feat: renderElement에서 기존 DOM 요소 교체 기능 추가
nogy21 Dec 23, 2024
cd6e49e
refactor: createVNode 불필요한 로직 제거 및 normalizeChildren 함수 내 문자열 병합 로직 별…
nogy21 Dec 23, 2024
c9c31fc
refactor: createElement 함수의 속성 업데이트 로직을 updateAttributes 함수로 분리 및 add…
nogy21 Dec 23, 2024
9585859
feat: renderElement에서 가상 DOM 업데이트 로직 추가 및 전역 상태 관리 구현
nogy21 Dec 23, 2024
1648acc
feat: updateElement 함수에 가상 DOM 비교, DOM 업데이트 및 updateAttributes 함수 로직 구현
nogy21 Dec 23, 2024
f9906d3
refactor: renderElement 함수 내 VNode 초기화 로직 initGlobalVNode 함수로 추출
nogy21 Dec 24, 2024
cdf24c5
docs: 함수 주석 추가
nogy21 Dec 24, 2024
4d8a999
refactor: normalizeVNode 함수 로직 개선
nogy21 Dec 24, 2024
d87ca6e
feat: on 이벤트 속성 추가/제거 함수 및 on 이벤트 속성명 추출 함수 추가
nogy21 Dec 25, 2024
1ddac4e
refactor: 이벤트 리스너 관리 로직을 WeakMap으로 개선하여 중복 등록 개선 및 메모리 누수 방지
nogy21 Dec 25, 2024
1ff9d2a
fix: normalizeVNode 함수 내 함수 타입 정규화 로직 개선 및 불필요한 함수 제거
nogy21 Dec 25, 2024
594ca22
feat: 홈페이지 로그인 여부에 따른 게시물 조작 구현
nogy21 Dec 26, 2024
e1f9a4d
feat: 게시물 제출 이벤트 처리 구현
nogy21 Dec 26, 2024
62cc7ac
refactor: 게시물 좋아요 토글 기능 외부 함수로 추출
nogy21 Dec 26, 2024
daca105
refactor: 게시물 좋아요 및 제출 기능을 globalStore 액션으로 통합
nogy21 Dec 26, 2024
0f3f3b2
fix: 이벤트 핸들러 추가 및 제거 로직 개선
nogy21 Dec 26, 2024
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
7 changes: 7 additions & 0 deletions src/components/posts/Post.jsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
/** @jsx createVNode */
import { createVNode } from "../../lib";
import { globalStore } from "../../stores/index.js";
import { toTimeFormat } from "../../utils/index.js";

export const Post = ({
id,
author,
time,
content,
likeUsers,
activationLike = false,
}) => {
const { toggleLike } = globalStore.actions;

return (
<div className="bg-white rounded-lg shadow p-4 mb-4">
<div className="flex items-center mb-2">
Expand All @@ -21,6 +25,9 @@ export const Post = ({
<div className="mt-2 flex justify-between text-gray-500">
<span
className={`like-button cursor-pointer${activationLike ? " text-blue-500" : ""}`}
onClick={() => {
toggleLike(id);
}}
>
좋아요 {likeUsers.length}
</span>
Expand Down
17 changes: 15 additions & 2 deletions src/components/posts/PostForm.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
/** @jsx createVNode */
import { createVNode } from "../../lib";
import { globalStore } from "../../stores";

const handleSubmit = (e) => {
e.preventDefault();
const content = document.getElementById("post-content").value;
if (!content) {
return;
}

const { submitPost } = globalStore.actions;
submitPost(content);
};

export const PostForm = () => {
return (
<div className="mb-4 bg-white rounded-lg shadow p-4">
<form className="mb-4 bg-white rounded-lg shadow p-4">
<textarea
id="post-content"
placeholder="무슨 생각을 하고 계신가요?"
Expand All @@ -12,9 +24,10 @@ export const PostForm = () => {
<button
id="post-submit"
className="mt-2 bg-blue-600 text-white px-4 py-2 rounded"
onClick={handleSubmit}
>
게시
</button>
</div>
</form>
);
};
7 changes: 7 additions & 0 deletions src/errors/TypeError.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export class TypeError extends Error {
static MESSAGE = "TypeError";

constructor() {
super(TypeError.MESSAGE);
}
}
81 changes: 79 additions & 2 deletions src/lib/createElement.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,82 @@
import { addEvent } from "./eventManager";

export function createElement(vNode) {}
import { isString } from "../utils/isString";
import { isValid } from "../utils/isValid";

function updateAttributes($el, props) {}
/**
* 가상 노드(vNode)를 실제 DOM 노드로 생성하는 함수
* @description
* 1. 가상 노드에 대한 유효성 검사 후 유효하지 않을 경우 빈 텍스트 노드 생성
* 2. 가상 노드가 문자열 또는 숫자인 경우 텍스트 노드로 생성
* 3. 가상 노드가 배열인 경우 fragment로 감싸서 생성
* 4. 그 외의 경우 createElement 함수를 통해 DOM 노드 생성
* 4-1. 속성 업데이트
* 4-2. 자식 노드 생성
* @param {*} vNode 가상 노드
* @returns {HTMLElement|Text|DocumentFragment} DOM 노드
*/
export function createElement(vNode) {
if (!isValid(vNode)) {
return document.createTextNode("");
}
if (isString(vNode)) {
return document.createTextNode(vNode);
}

if (Array.isArray(vNode) && vNode.length > 1) {
return createFragmentWithArray(vNode);
}

const $el = document.createElement(vNode.type);

if (vNode.props && Object.keys(vNode.props).length > 0) {
updateAttributes($el, vNode.props);
}

if (vNode.children) {
vNode.children.forEach((child) => {
const $child = createElement(child);
$el.appendChild($child);
});
}

return $el;
}

/**
* DOM 엘리먼트의 속성을 업데이트하는 함수
* @description 업데이트할 속성을 반복문을 통해 순회하며 업데이트
* 1. className 속성은 class로 변경
* 2. on으로 시작하는 속성은 이벤트 핸들러로 등록
* 3. 그 외의 속성은 setAttribute로 업데이트
* @param {*} $el DOM 엘리먼트
* @param {*} props 업데이트할 속성
*/
function updateAttributes($el, props) {
Object.entries(props).forEach(([key, value]) => {
if (key === "className") {
$el.setAttribute("class", value);
return;
}
if (key.startsWith("on")) {
const event = key.slice(2).toLowerCase();
addEvent($el, event, value);
return;
}
$el.setAttribute(key, value);
});
}

/**
* 배열 형태의 vNode를 받을 경우 fragment로 감싸서 반환
* @param {*} vNode
* @returns {DocumentFragment} fragment
*/
function createFragmentWithArray(vNode) {
const fragment = document.createDocumentFragment();
vNode.forEach((node) => {
const $el = createElement(node);
fragment.appendChild($el);
});
return fragment;
}
19 changes: 18 additions & 1 deletion src/lib/createVNode.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
/**
* 가상 노드를 생성하는 함수
* @description type, props, children을 인자로 받아 가상 노드를 생성
* - children 요소 중 0, null, undefined는 제외
* - children을 flat하게 만들고, 각 요소를 flatMap
* @param {*} type
* @param {*} props
* @param {...any} children
* @returns {Object} vNode
*/
export function createVNode(type, props, ...children) {
return {};
return {
type,
props,
children: children
.flat(Infinity)
.filter((child) => (child === 0 || child ? true : false))
.flatMap((child) => child),
};
}
69 changes: 66 additions & 3 deletions src/lib/eventManager.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,68 @@
export function setupEventListeners(root) {}
const eventListeners = new Map();

export function addEvent(element, eventType, handler) {}
/**
* root 요소에 이벤트 리스너를 등록하는 함수
* @description
* - 이벤트 위임을 통해 이벤트 핸들러를 등록
* - 메모리 누수 방지를 위해 이벤트 리스너를 해제하고 다시 등록
* @param {HTMLElement} root
*/
export function setupEventListeners(root) {
eventListeners.forEach((_, eventType) => {
root.removeEventListener(eventType, handleEvent);
root.addEventListener(eventType, handleEvent);
});
}

export function removeEvent(element, eventType, handler) {}
/**
* 이벤트 리스너를 등록하는 함수
* @param {*} element
* @param {*} eventType
* @param {*} handler
*/
export function addEvent(element, eventType, handler) {
if (typeof handler !== "function") {
throw new TypeError("Handler must be a function");
}

if (!eventListeners.has(eventType)) {
eventListeners.set(eventType, new WeakMap());
}

const listeners = eventListeners.get(eventType);
listeners.set(element, handler);
}

/**
* 이벤트 리스너를 해제하는 함수
* - WeakMap을 사용했기 때문에 eventListeners에 대한 삭제는 별도로 처리하지 않아도 된다.
* @description
* @param {*} element
* @param {*} eventType
* @param {*} handler
*/
export function removeEvent(element, eventType) {
if (!eventListeners.has(eventType)) {
return;
}

const elementEventListeners = eventListeners.get(eventType);
elementEventListeners.delete(element);
}

/**
* 이벤트 핸들러 처리 함수
*/
const handleEvent = (e) => {
const { target, type } = e;
const listeners = eventListeners.get(type);

if (!listeners) {
return;
}

const handler = listeners.get(target);
if (typeof handler === "function") {
handler(e);
}
};
30 changes: 30 additions & 0 deletions src/lib/normalizeVNode.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,33 @@
import { isString } from "../utils/isString";
import { isValid } from "../utils/isValid";

/**
* 가상 노드(vNode)를 표준 형식으로 정규화하는 함수
* @description 조건에 따라 vNode를 정규화하고, 정규화된 vNode를 반환한다.
* 1. vNode가 null, undefined, boolean인 경우 빈 문자열을 반환한다.
* 2. vNode가 문자열 또는 숫자인 경우 해당 값을 문자열로 반환한다.
* 3. vNode가 함수 타입인 경우 해당 함수를 호출하며 반환된 값을 정규화한다.
* 4. 모든 자식 노드를 정규화한다.
* @param {Object} vNode - 정규화할 가상 노드
* @returns {Object|string} 정규화된 가상 노드 또는 vNode가 유효하지 않은 경우 빈 문자열을 반환
*/
export function normalizeVNode(vNode) {
if (!isValid(vNode)) {
return "";
}
if (isString(vNode)) {
return vNode.toString();
}

if (typeof vNode.type === "function") {
return normalizeVNode(
vNode.type({ ...vNode.props, children: vNode.children }),
);
}

if (vNode.children && vNode.children.length) {
vNode.children = vNode.children.filter(isValid).map(normalizeVNode);
}

return vNode;
}
29 changes: 25 additions & 4 deletions src/lib/renderElement.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,31 @@
import { setupEventListeners } from "./eventManager";
import { createElement } from "./createElement";
import { setupEventListeners } from "./eventManager";
import { normalizeVNode } from "./normalizeVNode";
import { updateElement } from "./updateElement";

const vDom = new WeakMap();

/**
* vNode를 받아서 해당 vNode를 container에 렌더링
* @description
* - 이전 가상 DOM과 비교하기 위해 WeakMap을 사용하여 container에 대한 가상 DOM 저장
* - container에 가상 DOM이 존재하지 않는 경우, createElement를 통해 DOM 생성
* - 가상 DOM이 존재할 경우 새로운 가상 DOM과 비교하여 변경분만 업데이트
* @param {*} vNode
* @param {*} container
* @returns {void}
*/
export function renderElement(vNode, container) {
// 최초 렌더링시에는 createElement로 DOM을 생성하고
// 이후에는 updateElement로 기존 DOM을 업데이트한다.
// 렌더링이 완료되면 container에 이벤트를 등록한다.
const newVNode = normalizeVNode(vNode);

if (!vDom.has(container)) {
const el = createElement(newVNode);
container.appendChild(el);
} else {
const oldVNode = vDom.get(container);
updateElement(container, newVNode, oldVNode);
}

setupEventListeners(container);
vDom.set(container, newVNode);
}
Loading
Loading