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

[5팀 박소미] [Chapter 1-2] 프레임워크 없이 SPA 만들기 #49

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
29 changes: 29 additions & 0 deletions src/components/posts/Post.jsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,42 @@
/** @jsx createVNode */
import { createVNode } from "../../lib";
import { toTimeFormat } from "../../utils/index.js";
import { globalStore } from "../../stores/globalStore.js";

export const Post = ({
id,
author,
time,
content,
likeUsers,
activationLike = false,
}) => {
const { loggedIn, currentUser } = globalStore.getState();
const { setState } = globalStore;

// 현재 사용자가 좋아요를 눌렀는지 확인
const isLikedByCurrentUser = likeUsers.includes(currentUser?.username);

const handleLikeToggle = () => {
if (!loggedIn) {
alert("로그인 후 이용해주세요");
return;
}

const updatedPosts = globalStore.getState().posts.map((post) => {
if (post.id === id) {
const updatedLikeUsers = isLikedByCurrentUser
? post.likeUsers.filter((user) => user !== currentUser.username) // 좋아요 취소
: [...post.likeUsers, currentUser.username]; // 좋아요 추가

return { ...post, likeUsers: updatedLikeUsers };
}
return post;
});

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

export const PostForm = () => {
return (
<div className="mb-4 bg-white rounded-lg shadow p-4">
<textarea
id="post-content"
placeholder="무슨 생각을 하고 계신가요?"
className="w-full p-2 border rounded"
/>
<button
id="post-submit"
className="mt-2 bg-blue-600 text-white px-4 py-2 rounded"
>
게시
</button>
</div>
);
const { loggedIn, currentUser } = globalStore.getState();
const { setState } = globalStore;

let postContent = "";

const handleTextareaChange = (e) => {
postContent = e.target.value; // 텍스트를 업데이트
};

const handlePostSubmit = () => {
if (!postContent.trim()) {
alert("내용을 입력해주세요!");
return;
}

const newPost = {
id: Date.now(), // 고유 ID
author: currentUser.username, // 로그인한 사용자
time: Date.now(), // 현재 시간
content: postContent.trim(), // 작성된 내용
likeUsers: [], // 좋아요 초기화
};

// 상태 업데이트
const updatedPosts = [newPost, ...globalStore.getState().posts];
setState({ posts: updatedPosts });

// 텍스트 초기화
document.querySelector("#post-content").value = "";
Copy link

@YeongseoYoon-hanghae YeongseoYoon-hanghae Dec 28, 2024

Choose a reason for hiding this comment

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

리액트 렌더링 알고리즘에 따르면 텍스트 초기화를 element를 직접 조작하는것보다 state를 통해 관리하는 방식이 어떨까? 제안드려봅니다 ㅎㅎ

postContent = "";
};

if (loggedIn) {

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.

HomePage에서 글로벌 스토어를 가져와서 로그인 상태를 통해 렌더링하는 방식으로

{loggedIn && <PostForm/>}

return (
<div className="mb-4 bg-white rounded-lg shadow p-4">
<textarea
id="post-content"
placeholder="무슨 생각을 하고 계신가요?"
className="w-full p-2 border rounded"
onChange={handleTextareaChange}
/>
<button
id="post-submit"
className="mt-2 bg-blue-600 text-white px-4 py-2 rounded"
onClick={handlePostSubmit}
>
게시
</button>
</div>
);
}
};
47 changes: 45 additions & 2 deletions src/lib/createElement.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,48 @@
import { addEvent } from "./eventManager";

export function createElement(vNode) {}
export function createElement(vNode) {
if (typeof vNode === "function") {
throw Error("컴포넌트는 인자로 올 수 없습니다.");
}
if (vNode == null || typeof vNode === "boolean") {
return document.createTextNode("");
}

function updateAttributes($el, props) {}
if (typeof vNode === "string" || typeof vNode === "number") {
return document.createTextNode(String(vNode));
}

if (Array.isArray(vNode)) {
const fragment = document.createDocumentFragment();
vNode.forEach((child) => {
fragment.appendChild(createElement(child)); // 자식 노드들을 재귀적으로 추가
});
return fragment;
}

const { type, props, children } = vNode;

const element = document.createElement(type);

if (props) {
updateAttributes(element, props);
}

children.forEach((child) => {
element.appendChild(createElement(child));
});

return element;
}

function updateAttributes($el, props) {
Object.entries(props).forEach(([key, value]) => {
if (key.startsWith("on")) {
addEvent($el, key.slice(2).toLowerCase(), value);
} else if (key === "className") {
$el.setAttribute("class", value);
} else {
$el.setAttribute(key, value);
}
});
}
10 changes: 9 additions & 1 deletion src/lib/createVNode.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
export function createVNode(type, props, ...children) {
return {};
const flatChildren = children
.flat(Infinity)
.filter((child) => child || child === 0);

return {
type,
props,
children: flatChildren,
};
}
82 changes: 79 additions & 3 deletions src/lib/eventManager.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,81 @@
export function setupEventListeners(root) {}
// addEvent와 removeEvent를 통해 element에 대한 이벤트 함수를 어딘가에
// 저장하거나 삭제합니다.

export function addEvent(element, eventType, handler) {}
// setupEventListeners를 이용해서 이벤트 함수를 가져와서
// 한 번에 root에 이벤트를 등록합니다.

export function removeEvent(element, eventType, handler) {}
// 이벤트 저장
const eventStorage = {};
// const eventStorage = new Map();

export function setupEventListeners(root) {
Object.keys(eventStorage).forEach((eventType) => {
root.addEventListener(eventType, eventHandlers);
});
}
// export function setupEventListeners(root) {
// eventStorage.forEach((handlerMap, eventType) => {
// root.addEventListener(eventType, eventHandlers);
// });
// }

export function addEvent(element, eventType, handler) {
if (!eventStorage[eventType]) {
eventStorage[eventType] = new Map();
}

const eventsMap = eventStorage[eventType];
eventsMap.set(element, handler);

// if (!eventStorage.has(eventType)) {
// eventStorage.set(eventType, new Map());
// }

// const handlerMap = eventStorage.get(eventType);
// handlerMap.set(element, handler);
}

export function removeEvent(element, eventType) {
if (eventStorage[eventType]) {
const eventsMap = eventStorage[eventType];
eventsMap.delete(element);

if (eventsMap.size === 0) {
delete eventStorage[eventType];
}
}
}
// export function removeEvent(element, eventType) {
// if (eventStorage[eventType]) {
// const handlerMap = eventStorage[eventType];
// handlerMap.delete(element);

// if (handlerMap.size === 0) {
// delete eventStorage[eventType];
// }
// }
// }

const eventHandlers = (e) => {
if (!eventStorage[e.type]) {
return;
}
const handlerGroup = eventStorage[e.type];
const handler = handlerGroup.get(e.target);

if (handler) {
handler(e);
}
};

// const eventHandlers = (e) => {
// if (!eventStorage[e.type]) {
// return;
// }
// const handlerMap = eventStorage[e.type];
// const handler = handlerMap.get(e.target);

// if (handler) {
// handler(e);
// }
// };
29 changes: 29 additions & 0 deletions src/lib/normalizeVNode.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,32 @@
export function normalizeVNode(vNode) {
if (vNode == null || typeof vNode === "boolean") {
return "";
}

if (typeof vNode === "string" || typeof vNode === "number") {
return String(vNode);
}

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

if (typeof vNode === "object") {
const { type, props, children } = vNode;

const normalizedChildren = children
.flat(Infinity)
.map(normalizeVNode)
.filter((child) => child !== "" && child != null);

return {
type,
props: props,
children: normalizedChildren,
};
}

return vNode;
}
18 changes: 17 additions & 1 deletion src/lib/renderElement.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,24 @@ import { createElement } from "./createElement";
import { normalizeVNode } from "./normalizeVNode";
import { updateElement } from "./updateElement";

let oldNode = null;

export function renderElement(vNode, container) {
// vNode 표준화
vNode = normalizeVNode(vNode);

if (!container.childNodes[0]) {
oldNode = null;
}

// 최초 렌더링시에는 createElement로 DOM을 생성하고
// 이후에는 updateElement로 기존 DOM을 업데이트한다.
if (!oldNode) {
container.append(createElement(vNode));
} else {
// 이후에는 updateElement로 기존 DOM을 업데이트한다.
updateElement(container, vNode, oldNode);
}
// 렌더링이 완료되면 container에 이벤트를 등록한다.
setupEventListeners(container);
oldNode = vNode;
}
Loading
Loading