23.06.08
오늘 할 일
- 알고리즘 문제 풀이
- 디지털콘텐츠기획 발표
- 리액트 만들기 스터디 최종 프로젝트 진행
- 카공실록 api 연결
TIL -> 블로그 형식으로?
블로그 쓸 때 TIL 참고 많이 하고 있다. 찾기 좀 불편해서 블로그 형식으로 바꿔야겠다고 생각했다.
gatsby나 astro 쓰면 되지 않을까?
대신 마크다운에 frontmatter를 달아야 한다. 요런 느낌으로
---
title:
category:
tags:
---
음.. 이걸 작성할 때마다 입력하면 귀찮을 것 같다. 자동화할 수 있는 방법을 찾아봐야겠다.
일단 기존에 있는 파일들은 chatGPT 사용하면 금방 만들어 줄 듯 하다!
방학 때 시간나면 만들어봐야겠다.
리액트 만들기 스터디 최종 프로젝트 진행
또두리스트 만들어보자!
홈 → 투두리스트
레이아웃 컴포넌트를 적용하고 싶다!
react-router-dom에서는 Outlet
컴포넌트로 가능하다.
export function Outlet(props: OutletProps): React.ReactElement | null {
return useOutlet(props.context);
}
export function useOutlet(context?: unknown): React.ReactElement | null {
let outlet = React.useContext(RouteContext).outlet;
if (outlet) {
return <OutletContext.Provider value={context}>{outlet}</OutletContext.Provider>;
}
return outlet;
}
아하 Provider로 감싸는구나
createRouter 수정
그 전에, 기존에 만들었던 createRouter를 수정해보자.
createBrowserRouter([
{
path: '/',
element: <Root />,
children: [
{
path: 'events/:id',
element: <Event />,
},
],
},
]);
children으로 감쌀 수 있구나! 나도 저렇게 해야겠다.
import { render } from '../Kreact';
export default function createRouter(root, routes = []) {
const history = window.history;
const routeMap = new Map();
routes.forEach(({ pathname, element, children }) => {
routeMap.set(pathname, { element, type: 'parent', parentPathname: null });
children &&
children.forEach(({ pathname: childPathname, element }) => {
if (childPathname.startsWith('/')) throw new Error('childPathname must not start with /');
routeMap.set(pathname + childPathname, {
element,
type: 'child',
parentPathname: pathname,
});
});
});
function push(pathname, state) {
history.pushState(state, null, pathname);
_render(root, pathname);
}
window.addEventListener('popstate', () => {
_render(root, window.location.pathname, true);
});
function _render(root, pathname, isPopState = false) {
const { element, type, parentPathname } = routeMap.get(pathname);
if (!element) throw new Error('NOT FOUND');
root.innerHTML = '';
render(root, element);
}
return {
push,
routes,
};
}
const router = createRouter(document.getElementById('root'), [
{
pathname: '/',
element: Home,
children: [
{
pathname: 'todolist',
element: TodoList,
},
],
},
]);
Outlet 추가
//Kreact-outer/Outlet.js
import { RouterContext } from '../../App';
import Kreact from '../Kreact';
export function Outlet({ pathname }) {
let router = null;
setTimeout(() => {
router = Kreact.useContext(RouterContext);
if (!router) {
return;
}
const route = router.routes.find((route) => route.pathname === pathname);
if (!route) {
throw new Error('NOT FOUND');
}
router.push(pathname, { outlet: true });
}, 0);
return <div id={pathname}></div>;
}
export default Outlet;
기존의 useContext의 문제가 있었다. 처음 렌더링할 때 context를 못받아와지는 문제가 있어서, setTimeout으로 세팅해서 렌더링이 완료되면 다시 context를 찾아서 재렌더링 시켜준다.
router.push
를 할 때 outlet: true로 설정하여 지금 push되는 것이 outlet이라고 알려주었다.
push 함수를 변경했다.
routes.forEach(({ pathname, element, children }) => {
routeMap.set(pathname, { element, type: 'parent', parentPathname: null, outlet: false });
children &&
children.forEach(({ pathname: childPathname, element }) => {
if (childPathname.startsWith('/')) throw new Error('childPathname must not start with /');
routeMap.set(pathname + childPathname, {
element,
type: 'child',
parentPathname: pathname,
outlet: false,
});
});
});
function push(pathname, state) {
history.pushState(state, null, pathname);
if (state?.outlet) {
routeMap.set(pathname, { ...routeMap.get(pathname), outlet: state.outlet ?? false });
}
_render(root, pathname);
}
수정한 것에 맞게 _render 함수도 수정했다.
function _render(root, pathname, isPopState = false) {
const { element, type, parentPathname, outlet } = routeMap.get(pathname);
if (!element) throw new Error('NOT FOUND');
if (isPopState || type === 'child') {
const key = type === 'child' ? parentPathname : pathname;
const { outlet } = routeMap.get(key);
if (outlet) {
document.getElementById(key).innerHTML = '';
render(document.getElementById(key), element);
return;
}
}
if (outlet && type === 'parent') {
render(document.getElementById(pathname), element);
return;
}
root.innerHTML = '';
render(root, element);
}
뒤로가기 할 때 isPopState를 true로 보내주었다.
window.addEventListener('popstate', () => {
_render(root, window.location.pathname, true);
});
최종
// App.js
import Layout from './components/Layout';
import Kreact from './core/Kreact';
import createRouter from './core/Kreact-router';
import Outlet from './core/Kreact-router/Outlet';
import Home from './pages/Home';
import TodoList from './pages/Todolist';
const router = createRouter(document.getElementById('root'), [
{
pathname: '/',
element: Home,
children: [
{
pathname: 'todolist',
element: TodoList,
},
],
},
]);
export const RouterContext = Kreact.createContext();
export default function App() {
return (
<Layout>
<RouterContext.Provider value={router}>
<Outlet pathname={window.location.pathname} />
</RouterContext.Provider>
</Layout>
);
}
홈
import { RouterContext } from '../../App';
import Kreact from '../../core/Kreact';
import styles from './index.module.css';
export default function Home() {
const router = Kreact.useContext(RouterContext);
return (
<div className={styles.home}>
<button className={styles.home__button} onClick={() => router.push('/todolist')}>
또두리스트로 가기
</button>
</div>
);
}
투두리스트
import Kreact from '../../core/Kreact';
import styles from './index.module.css';
export default function TodoList() {
const [list, setList] = Kreact.useState([]);
const handleSubmit = (e) => {
e.preventDefault();
const value = e.target[0].value;
if (value === '') {
alert('할 일을 입력해주세요');
return;
}
setList((prev) => {
return [...prev, { id: prev.length, todo: value, idDone: false }];
});
e.target[0].value = '';
};
const handleCheckClick = (id) => {
setList((prev) => {
return prev.map((item) => {
if (item.id === id) {
return { ...item, idDone: !item.idDone };
}
return item;
});
});
};
const handleDeleteClick = (id) => {
setList((prev) => {
return prev.filter((item) => item.id !== id);
});
};
return (
<div className={styles.todolist}>
<h1 className={styles.todolist__title}>할 일을 적어보아요</h1>
<form className={styles.todolist__form} onSubmit={handleSubmit}>
<input
type='text'
placeholder='할 일을 적으세요'
className={styles.todolist__form__input}
/>
<button className={styles.todolist__form__add}>추가</button>
</form>
<ul className={styles.todolist__list}>
{list.map((item, index) => (
<li key={index} className={styles.todolist__item}>
<div style={{ display: 'flex' }}>
{item.idDone ? (
<input type='checkbox' onClick={() => handleCheckClick(item.id)} checked />
) : (
<input type='checkbox' onClick={() => handleCheckClick(item.id)} />
)}
<span className={styles.todolist__item__text}>{item.todo}</span>
</div>
<button
className={styles.todolist__button__delete}
onClick={() => handleDeleteClick(item.id)}
>
삭제
</button>
</li>
))}
</ul>
</div>
);
}
트러블슈팅
엇 왜 삭제가 안되지??
export function updateVirtualDOM(root, oldNode, newNode, commitMap, index = 0) {
if (!oldNode) return commitMap.set('appendChild', { root, child: createVirtualDOM(newNode) });
if (!newNode)
return commitMap.set('removeChild', { root, child: createVirtualDOM(root.childNodes[index]) });
if (oldNode.type !== newNode.type)
return commitMap.set('replaceChild', {
root,
newChild: createVirtualDOM(newNode),
oldChild: root.childNodes[index],
});
if (
oldNode.type === 'TEXT_ELEMENT' &&
newNode.type === 'TEXT_ELEMENT' &&
oldNode.props.nodeValue !== newNode.props.nodeValue
) {
const newElement = createVirtualDOM(newNode);
return commitMap.set('replaceChild', {
root,
newChild: newElement,
oldChild: root.childNodes[index],
});
}
const oldProps = oldNode.props;
const newProps = newNode.props;
if (oldNode.type !== 'TEXT_ELEMENT' && oldNode.type !== 'FRAGMENT') {
getElementWithStyle(root.childNodes[index], newProps, oldProps);
}
const max = Math.max(oldProps.children.length, newProps.children.length);
for (let i = 0; i < max; i++) {
updateVirtualDOM(
oldNode.type === 'FRAGMENT' ? root : root.childNodes[index],
oldProps.children[i],
newProps.children[i],
commitMap,
i
);
}
return root;
}
createVirtualDOM 으로 넘어가는 인자가 Node라서 에러가 떴다. 넘어가는게 KreactElement 객체 형태로 넘어가야 한다.
애초에 child가 Node 형태여야돼서 createVirtualDOM을 감싸지 않고 그냥 root.childNodes[index]를 넣어주니 해결!
내일 할 일
- 알고리즘 문제 풀이
- 부스트캠프 자소서 작성
- 카공실록 API 연결