← Dev Log

2023년 06월 08일

·8 min read

오늘 할 일

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