React To Do App 정리

한대희·2023년 5월 17일
0

구현한 기능

1.전체 To do 리스트 보여주기
2.To do 추가
3.To do 삭제
4.To do 체크박스
5.To do 리스트 필터링
6.다크모드
7.로컬스토리지에 저장

컴포넌트 구조

중요 Skill

1. context api를 활용한 다크 모드 구현 with Tailwind CSS

여러 컴포넌트에서 공통적으로 필요한 state가 있을때 이걸 전역적으로 선언을 해서 props drilling없이 한번만 불러와서 사용할 수 있게 하는 것이 context api다. 다크 모드의 state는 모든 컴포넌트에서 필요하기 때문에 context api를 활용하여 다크 모드를 구현해 보았다.

context를 사용하기 위해서는 먼저 context를 만들고, 해당 cotext를 사용할 컴포넌트를 provider로 감싸 주면 된다.

import { createContext, useContext, useEffect, useState } from 'react'

//😀 1. 먼저 createContext로 context를 만들고

const DarkModeContext = createContext()

//😀 2. provider를 export하기 위해 함수로 만들어 준다.

export function DarkModeProvider({ children }) {
//😀 3. 공통적으로 사용할 state를 지정해 준다.
  const [darkMode, setDarkMode] = useState(false)

//😀 4. provider로 감싸진 자식 node에서 darkMode라는 state를 변경할 수 있는 함수를 만들어 준다.
  const toggleDarkMode = () => {
    setDarkMode(prev => !prev)
    updateDarkMode(!darkMode)
  }

  
  return (
//😀 5. context에서 제공하는 provider로 children을 감싸주고 value에 darMode라는 state와 해당 state를 업데이트 할 수 있는 함수를 내려 주면 provider로 감싸진 node에서 state와 함수를 불러와 사용할 수 있다.
  <DarkModeContext.Provider value={{darkMode, toggleDarkMode}}>
    {children}
  </DarkModeContext.Provider>
)
}


// context를 사용하고자 하는 컴포넌트에서 useContext()를 사용하여 소괄호 안에 
어떤 context를 사용할 것인지를 입력해 줘야 하는데 아래 처럼 훅을 만들어 주면 사용하는 곳에서 
어떤 context를 사용할지 생각하지 않고 해당 훅만 불러와서 좀 더 쉽게 사용할 수 있다.

export const useDarkMode = () => useContext(DarkModeContext)

이렇게 만들어둔 darkMode context는 모든 컴포넌트에서 필요로 하므로 App.js전체를 Provider로 감싸준다.

function App() {
  const [filter, setFilter] = useState(filters[0]);
  
  const filterChangeHandler = (filter) => {
    setFilter(filter)
  }
  return (
    <DarkModeProvider>
      <Header filters={filters} filter={filter} onFilterChange={filterChangeHandler} />
      <TodoList filter={filter} />
    </DarkModeProvider>
  );
}

이제 만들어둔 context를 불러와 보자

import React from 'react';
import styles from './Header.module.css'
import { useDarkMode } from '../../context/DarkModeContext';
import {HiMoon, HiSun} from 'react-icons/hi'


export default function Header({ filters, filter, onFilterChange }) {

// 😀만들어둔 useDarkMode훅을 불러와 darkMode라는 state와 해당 state를 업데이트 할 수 있는 
   toggleDarkMode라는 함수를 불러 왔다.
  
  const {darkMode, toggleDarkMode} = useDarkMode()

  return (
    <header className={styles.header}>
 // 😀darkMode라는 state가 true면 달 모양을 false면 해 모양 icon이 나타나게 하였다.
 그리고 버튼의 onClick이벤트에 context에서 가져온 함수를 넣어 준다. 
      <button onClick={toggleDarkMode} className={styles.toggle}>
		{!darkMode && <HiMoon />}
        {darkMode && <HiSun />}
      </button>
      <ul className={styles.filters}>
        {filters.map((value, index) => (
          <li key={index} >
            {/* 버튼은 기본적으로 filter라는 class이름을 가지지만, 만약 전달 받은 filter와 filters의 각 요소중 같은 것이 있다면 selected라는 class도 추가적으로 가질 수 있게 해준 것 */}
            <button 
            className={`${styles.filter} ${
                filter === value && styles.selected
              }`} onClick={() => onFilterChange(value)}>{value}</button>
          </li>
        ))}
      </ul>
    </header>
  );
}

여기 까지 했으면 Header컴포넌트에서 버튼 클릭시 달모양과 해모양이 나타날 것이다. 그러면 이제 실질적으로 ui가 다크모드, 라이트모드가 되도록 해보자.

  //😀 먼저 darkMode context파일에 돌아가서 DarkModeProvider는 앱이 실행이 될때 처음 
  	  실행이 되므로, useEffect을 통해 
       어플리케이션이 실행이 될 때 현재 dark모드인지 아닌지 확인 한 다음에 초기값을 설정 해 준다.
  useEffect(() => {
  //😀 localStorage에 저장된 theme가 dark이거나, 브라우저 자체가 다크모드인지 아닌지 
       확인 한 다음에 둘중 하나라도 dark이면 true를 둘다 dark가 아니면 false를 isDark에 저장을 
       한다.
    const isDark = 
    (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches))
    console.log(isDark)
  //😀 현재 상태를 isDark로 업데이트 해주고 
    setDarkMode(isDark)

테일윈드 공식문서에 따라 darkmode시 dark라는 클래스를 추가하고, light모드시 dark라는 클래스를 제거 해서 ui를 구현할 것이다.

//😀 먼저 darkMode context파일에 가서 html요소에 dark라는 클래스를 추가하고 제거 할 수 있는
     함수를 만들어 준다.
function updateDarkMode(darkMode) {
  if(darkMode) {
    document.documentElement.classList.add('dark')
    localStorage.theme = 'dark'
  } else {
    document.documentElement.classList.remove('dark')
    localStorage.theme = 'light'
  }
}

//😀 그리고 위에서 작성한 useEffect에 해당 함수를 넣고 인자로 현재 다크모드인지 아닌지 전달한다.
  useEffect(() => {
    const isDark = 
    (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches))
    console.log(isDark)
	setDarkMode(isDark)
	updateDarkMode(isDark)
  }, [])

여기 까지 내용을 정리해 보자면 darkMode라는 context를 만들었고, 여기에 darkMode라는 상태와 toggleDarkMode라는 상태 업데이트 함수를 만들었고, 이것들을 provider의 자식 노드로 전달을 했다.
그리고 자식 노드인 Header 컴포넌트에서 버튼 클릭시 darkMode라는 상태와, html요소에 darkMode가 true면 dark라는 클래스가 추가 되도록 하고 현재 상태를 local스토리지에 저장되게 하였다.

import { createContext, useContext, useEffect, useState } from 'react'


const DarkModeContext = createContext()


export function DarkModeProvider({ children }) {
  const [darkMode, setDarkMode] = useState(false)
  
  const toggleDarkMode = () => {
    setDarkMode(prev => !prev)
    updateDarkMode(!darkMode)
  }

  useEffect(() => {
  	const isDark = 
    (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches))
	setDarkMode(isDark)
	updateDarkMode(isDark)
  }, [])
  
  return (
  <DarkModeContext.Provider value={{darkMode, toggleDarkMode}}>
    {children}
  </DarkModeContext.Provider>
)
}



function updateDarkMode(darkMode) {
  if(darkMode) {
    document.documentElement.classList.add('dark')
    localStorage.theme = 'dark'
  } else {
    document.documentElement.classList.remove('dark')
    localStorage.theme = 'light'
  }
}


export const useDarkMode = () => useContext(DarkModeContext)

이제 정말 ui를 dark모드 light모드에 맞게 변경해 보자.
먼저 해당 어플리케이션의 색상들은 모두 변수로 지정이 되어 있다.

:root {
  --color-bg-dark: #f5f5f5;
  --color-bg: #fdfffd;
  --color-grey: #d1d1d1;
  --color-text: #22243b;
  --color-accent: #fb29d1;
  --color-white: white;
  --color-scrollbar: #aaa7a7;
}
// 여기에 아래의 코드를 추가하여 html태그에 dark라는 클래스가 있다면 해당 변수의 색을 사용하고
   있는 요소들의 색을 변경해 주면 된다.
html.dark {
  --color-bg-dark: #1a1c35;
  --color-bg: #1a1c35;
  --color-grey: #4e4e4e;
  --color-text: #fdfffd;
}

2. localStorage

투두리스트를 추가한 후 페이지를 새로 고침 하면 기존에 추가했던 데이터 들이 모두 사라지는 문제가 있기 때문에 현재 데이터를 로컬스토리지에 저장을 했다.

먼저 todos라는 state가 변경이 될 때 마다 로컬 스토리지에 저장을 하도록 하였다. 페이지가 처음 렌더링이 될 때, 업데이트 될 때, 언마운트가 될 때 무언갈 하고 싶다면 useEffect를 사용해야 한다.

  //😀 useEffect을 통해 todos가 변할 때 마다 로컬스토리지의 todos라는 key에 데이터를 저장
       하도록 하였다.
  //😀 로컬 스토리지에 객체를 저장하기 위해서는 json으로 변환을 해줘야 한다.
  useEffect(()=>{
    localStorage.setItem('todos', JSON.stringify(todos) )
  },[todos])

그리고 todos라는 state에 초기값에 로컬 스토리지에서 불러온 데이터를 전달하면 될 것이다.
먼저 로컬 스토리지에서 데이터를 불러오는 함수를 만든다.

//😀 로컬 스토리지에 todos가 있다면 json으로 변환해서 todos에 저장을 하고
     없다면 빈 배열을 할당 한 다음 todos리턴 하게 하고, 초기값으로 아래의 함수를 전달한다.
  
  function todosFromLocalStorage () {
    const todos = localStorage.getItem('todos')
    return todos ? JSON.parse(todos) : []
  }
  
  const [todos, setTodos] = useState(() => todosFromLocalStorage())

여기서 중요한 점이 있다. 왜 초기값을 전달 할때 todosFromLocalStorage()이렇게 전달하지 않고 () => todosFromLocalStorage() 이렇게 콜백함수로 전달을 했을까??

그 이유는 useState의 특징 때문이다. todos라는 상태가 업데이트가 되면 해당 컴포넌트 전체가 다시 호출이 될 것이고, 그렇게 되면 useState도 다시 실행이 되어 초기값이 다시 전달이 될 것인데 useState는 기존의 값을 기억하고 있어서 useState가 다시 실행이 될 때 이미 값이 존재하고 있다면 초기값이 다시 전달이 되지 않는다.

그렇기 때문에 만약 초기값으로 함수를 호출하여 어떤 값을 가져와 초기값으로 전달하게 된다면 해당 함수는 호출이 되지만 기존의 값이 존재하여 초기값이 다시 전달이 되지 않기 때문에 해당 함수가 불필요하게 호출이 되게 된다.

이걸 해결하려면 콜백함수안에서 함수를 호출하게 하면 되는데 이렇게 해주면 컴포넌트가 마운트 될 때만 실행이 되고, 업데이트가 되어도 더이상 호출되지 않는다.

profile
개발 블로그

0개의 댓글