본문으로 건너뛰기

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 연결