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

[2팀 송창엽] [Chapter 1-2] 프레임워크 없이 SPA 만들기 #45

Open
wants to merge 18 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
2 changes: 1 addition & 1 deletion index.hash.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.hash.jsx"></script>
<script type="module" src="/src/main.hash.tsx"></script>
</body>
</html>
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
15 changes: 15 additions & 0 deletions package-lock.json

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

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/user-event": "^14.5.2",
"@vitejs/plugin-react": "^4.3.4",
"@vitest/ui": "^2.1.8",
"@vitest/coverage-v8": "^2.1.8",
"@vitest/ui": "^2.1.8",
"eslint": "^9.16.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
Expand All @@ -44,6 +44,7 @@
"jsdom": "^25.0.1",
"lint-staged": "^15.2.11",
"prettier": "^3.4.2",
"typescript": "^5.7.2",

Choose a reason for hiding this comment

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

타입스크립트 사용하셨군요.. 저도 다음 과제에는 꼭 타입스크립트로 한번 해보겠습니다..

Choose a reason for hiding this comment

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

22..

Copy link

Choose a reason for hiding this comment

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

333

Copy link
Author

Choose a reason for hiding this comment

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

이번 과제부터 타입스크립트가 기본 적용이니 같이 타입지옥에 빠져보시지요 ㅎㅎㅎ

"vite": "^6.0.3",
"vitest": "^2.1.8"
}
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/chapter1-1/advanced.hashRouter.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ beforeAll(async () => {
// DOM 초기화
window.alert = vi.fn();
document.body.innerHTML = '<div id="root"></div>';
await import("../../main.hash.jsx");
await import("../../main.hash.tsx");
});

afterAll(() => {
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/chapter1-1/advanced.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ beforeAll(async () => {
// DOM 초기화
window.alert = vi.fn();
document.body.innerHTML = '<div id="root"></div>';
await import("../../main.jsx");
await import("../../main.tsx");
});

afterAll(() => {
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/chapter1-1/basic.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ beforeAll(async () => {
// DOM 초기화
window.alert = vi.fn();
document.body.innerHTML = '<div id="root"></div>';
await import("../../main.jsx");
await import("../../main.tsx");
});

afterAll(() => {
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/chapter1-2/advanced.post.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ beforeAll(async () => {
// DOM 초기화
window.alert = vi.fn();
document.body.innerHTML = '<div id="root"></div>';
await import("../../main.jsx");
await import("../../main.tsx");
});

afterAll(() => {
Expand Down
File renamed without changes.
27 changes: 25 additions & 2 deletions src/components/posts/Post.jsx → src/components/posts/Post.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,36 @@
/** @jsx createVNode */
import { createVNode } from "../../lib";
import { globalStore, PostType } from "../../stores/globalStore.js";
import { toTimeFormat } from "../../utils/index.js";

interface PostProps extends PostType {
activationLike?: boolean;
}

export const Post = ({
author,
time,
content,
likeUsers,
activationLike = false,
}) => {
id,
}: PostProps) => {
const { loggedIn } = globalStore.getState();
const { toggleLike } = globalStore.actions;
Comment on lines +18 to +19

Choose a reason for hiding this comment

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

Post를 호출하는 곳에서 조회하면 더 좋을 것 같아요
loggedIn 이라는 값은 globalStore에서 한번만 조회도 되는 값이라 Post에서 하는 것 보단 Post를 호출하는 부모 컴포넌트에서 사용해서 props으로 넘겨주는게 더 좋지 않을까 싶습니다.

Copy link
Author

Choose a reason for hiding this comment

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

앗 그러네요 ! 원영님 말씀대로 생각하니 더 좋은 구조인 것 같습니다
좋은의견 감사합니다 :)


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

if (!id) {
return;
}

toggleLike(id);
};

return (
<div className="bg-white rounded-lg shadow p-4 mb-4">
<div className="flex items-center mb-2">
Expand All @@ -21,8 +43,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={handleToggleLike}
>
좋아요 {likeUsers.length}
좋아요 {likeUsers?.length}
</span>
<span>댓글</span>
<span>공유</span>
Expand Down
20 changes: 0 additions & 20 deletions src/components/posts/PostForm.jsx

This file was deleted.

46 changes: 46 additions & 0 deletions src/components/posts/PostForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/** @jsx createVNode */
import { createVNode } from "../../lib";
import { globalStore } from "../../stores";
import { getCurrentTime } from "../../utils";

export const PostForm = () => {
const { addPost } = globalStore.actions;
const { currentUser } = globalStore.getState();

const handleSubmit = (event: SubmitEvent) => {
event.preventDefault();

if (!currentUser) return;

const postContent = document.getElementById(
"post-content",
) as HTMLTextAreaElement;

const contentValue = postContent ? postContent.value : "";

addPost({
author: currentUser.username,
content: contentValue,
time: getCurrentTime(),
});
};

return (
<form
className="mb-4 bg-white rounded-lg shadow p-4"
onSubmit={handleSubmit}
>
<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>
</form>
);
};
2 changes: 0 additions & 2 deletions src/components/posts/index.js

This file was deleted.

2 changes: 2 additions & 0 deletions src/components/posts/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./Post.tsx";
export * from "./PostForm.tsx";
23 changes: 23 additions & 0 deletions src/components/templates/Link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/** @jsx createVNode */
import { createVNode } from "../../lib";
import { router } from "../../router";

interface LinkProps {
onClick?: () => void;
children?: unknown;
href: string;
className?: string;
}
Comment on lines +5 to +10

Choose a reason for hiding this comment

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

저는 타입스크립트를 많이 안 써봤습니다..ㅎㅎ
그래서 궁금한게 이렇게 보통 props를 넘겨줄 때, interface 아니면 type도 쓰는 것 같던데 보통 뭘 더 많이 쓰시나요??

Choose a reason for hiding this comment

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

https://yceffort.kr/2021/03/typescript-interface-vs-type
저도 이게 궁금해서 찾아보다가 도움이 되었던 블로그를 공유드려요! 🤓

Copy link
Author

Choose a reason for hiding this comment

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

수연님 좋은 아티클 감사드립니다 ㅎㅎ

이건 별건 아니구 타입스크립트의 문제를 풀어보는 챌린지인데 한번 보셔도 될 것 같습니다 ! (저는 eazy도 어려웠어요)
타입스크립트에 익숙해지는데 도움이 되는 것 같습니다 :)

https://github.com/type-challenges/type-challenges


export function Link({ onClick, children, ...props }: LinkProps) {
const handleClick = (e) => {
e.preventDefault();
onClick?.();
Copy link

Choose a reason for hiding this comment

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

저는 보통 onClick && onClick() 이렇게 사용했었는데, onClick?.() 이렇게 쓰면 onClick이 함수인 경우에만 실행되는 것이라서 더 안전하다고 하네요!
알아갑니다!! 👍👍

router.get().push(e.target.href.replace(window.location.origin, ""));
};
return (
<a onClick={handleClick} {...props}>
{children}
</a>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,13 @@
import { createVNode } from "../../lib";
import { router } from "../../router";
import { globalStore } from "../../stores";
import { Link } from "./Link";

const getNavItemClass = (path) => {
const currentPath = window.location.pathname;
return currentPath === path ? "text-blue-600 font-bold" : "text-gray-600";
};

function Link({ onClick, children, ...props }) {
const handleClick = (e) => {
e.preventDefault();
onClick?.();
router.get().push(e.target.href.replace(window.location.origin, ""));
};
return (
<a onClick={handleClick} {...props}>
{children}
</a>
);
}

export const Navigation = () => {
const { loggedIn } = globalStore.getState();
const { logout } = globalStore.actions;
Expand Down
File renamed without changes.
5 changes: 0 additions & 5 deletions src/lib/createElement.js

This file was deleted.

79 changes: 79 additions & 0 deletions src/lib/createElement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { addEvent } from "./eventManager";
import { normalizeVNode } from "./normalizeVNode";

export interface VNode {
type: string | Function;
props: Record<string, any>;
children?: Array<VNode | string | number>;
}

export function createElement(vNode: VNode) {
if (vNode && typeof vNode.type === "function") {
throw new Error("Function component is not supported");
}

// vNode 정규화
let normalizedVNode = normalizeVNode(vNode);

// null, undefined, boolean 빈 텍스트 노드 변환
if (
typeof normalizedVNode === "boolean" ||
normalizedVNode === null ||
normalizedVNode === undefined
) {
return document.createTextNode("");
}

if (
typeof normalizedVNode === "string" ||
typeof normalizedVNode === "number"
) {
return document.createTextNode(String(normalizedVNode));
}

if (Array.isArray(normalizedVNode)) {
const fragment = document.createDocumentFragment();

normalizedVNode.forEach((child) => {
fragment.appendChild(createElement(child));
});

return fragment;
}

// 일반 DOM 요소 생성
const { type, props = {}, children = [] } = normalizedVNode;
const $element = document.createElement(type) as HTMLElement;

// props 적용
updateAttributes($element, props);

// children 추가
children.forEach((child) => {
$element.appendChild(createElement(child));
});

return $element;
}

function updateAttributes($element: HTMLElement, props: Record<string, any>) {
if (!props) return;

Object.entries(props).forEach(([key, value]) => {
// 이벤트 핸들러 등록
if (key.startsWith("on") && typeof value === "function") {
const eventType = key.toLowerCase().substring(2);
addEvent($element, eventType, value);
return;
}

// className 등록
if (key === "className") {
$element.setAttribute("class", value);
return;
}

// 일반 속성 등록
$element.setAttribute(key, value);
});
}
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion src/lib/createRouter.js → src/lib/createRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const createRouter = (routes) => {
const getTarget = () => routes[getPath()];

const push = (path) => {
window.history.pushState(null, null, path);
window.history.pushState(null, "", path);
notify();
};

Expand Down
File renamed without changes.
23 changes: 0 additions & 23 deletions src/lib/createStore.js

This file was deleted.

Loading
Loading