오늘 할 일
- 알고리즘 문제 풀이
- 디지털콘텐츠기획 발표
- 리액트 만들기 스터디 최종 프로젝트 진행
- 카공실록 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 연결