TIL_2024_02_09

이종현·2024년 2월 9일
0

Today_I_Learned

목록 보기
135/145
post-thumbnail

Today 요약

  1. Next 공부
  2. 프로젝트
  3. 강의

1. What I Learned?

Next.js

passHref

Next의 Link href를 하위 컴포넌트에 전달하기 위함이다. 일반적으로 Link 안에 텍스트가 들어있거나 Next의 다른 요소들이 들어있으면 passHref를 전달하지 않아도 제대로 동작하는 것 같다. 하지만 스타일 컴포넌트나 이모션으로 정의한 컴포넌트의 경우에는 전달해서 사용해야 한다.

Zustand

상태관리 라이브러리 중에서 redux-toolkit으로 redux는 어느 정도 사용해봤다. 두 개의 프로젝트에서 사용해봤고, 엄청나게 이해도가 높은 건 아니지만 그래도 다음에 redux로 다시 프로젝트를 한다면 어느 정도는 잘 활용할 수 있을 거라 생각한다. 그리고 지금 진행하고 있는 프로젝트는 recoil이다. redux에 비하면 엄청 편한다는 건 간단하게 사용해 본 것만으로도 알 수 있다. 그런데 이번에 강의에서 zustand로 todo리스트 간단하게 구현하는 부분이 있어서 최종적으로 모두 사용해 본 다음에 차이점을 한 번 정리해보려고 한다. 그 전에 일단 오늘 간단하게 구현해본 부분을 정리해보자.

설치

설치도 간단하다. 아래 명령어로 설치하면 된다.

yarn add zustand

사용법

사용방법은 원하는 곳에서 사용하기 위해서 store를 생성한다. 하나의 프로젝트에서 여러개의 store를 생성해서 관리할 수 있다.

create 명령어로 생성할 수 있고, 그 안에 상태값들을 정의한다.

useTodoStore.ts

import { v4 as uuid } from 'uuid'
import { create } from 'zustand'

export interface TodoValue {
  id: string
  text: string
  isComplete: boolean
}

export interface TodoProps {
  todos: TodoValue[]
  addTodo: (todoText: string) => void
  completeTodo: (id: string, checked: boolean) => void
  deleteTodo: (id: string) => void
}

const useTodoStore = create<TodoProps>((set) => ({
  todos: [],
  addTodo: (todoText) =>
    set((state) => ({
      todos: [
        ...state.todos,
        {
          id: uuid(),
          text: todoText,
          isComplete: false
        }
      ]
    })),
  completeTodo: (todoId, checked) =>
    set((state) => ({
      todos: state.todos.map((todo) =>
        todo.id === todoId ? { ...todo, isComplete: checked } : todo
      )
    })),
  deleteTodo: (todoId) =>
    set((state) => ({
      todos: state.todos.filter((todo) => todo.id !== todoId)
    }))
}))

export default useTodoStore

todos 배열과 todos 배열과 관련된 메서드를 정의했다.

Todo.tsx

이제 정의한 부분을 사용하려면 정의한 store를 그대로 가지고 와서 state를 호출해서 사용하면 된다.

const todos = useTodoStore((state) => state.todos)
import TodoForm from '@/components/(todo)/TodoForm'
import TodoList from '@/components/(todo)/TodoList'
import useTodoStore from '@store/useTodoStore'

const Todo = () => {
  const todos = useTodoStore((state) => state.todos)

  return (
    <section
      style={{
        width: '300px',
        height: '500px',
        margin: 'auto',
        marginTop: '100px',
        marginBottom: '100px',
        border: '1px solid black'
      }}
    >
      <TodoForm />
      <TodoList todos={todos} />
    </section>
  )
}

export default Todo

TodoForm.tsx

import { ChangeEvent, FormEvent, useState } from 'react'
import useTodoStore from '@/store/useTodoStore'

const TodoForm = () => {
  const [todoText, setTodoText] = useState('')
  const addTodo = useTodoStore((state) => state.addTodo)

  const handleSubmit = (e: FormEvent) => {
    e.preventDefault()

    addTodo(todoText)
  }

  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    setTodoText(e.target.value)
  }

  return (
    <form onSubmit={handleSubmit} style={{ marginTop: '30px' }}>
      <input
        type="text"
        id="new-todo"
        name="newTodo"
        value={todoText}
        onChange={handleChange}
      />
      <button type="submit">추가하기</button>
    </form>
  )
}

export default TodoForm

TodoList.tsx

import TodoItem from '@components/(todo)/TodoItem'
import { TodoValue } from '@store/useTodoStore'

interface TodoListProps {
  todos: TodoValue[]
}

const TodoList = ({ todos }: TodoListProps) => {
  return (
    <ul>
      {todos.map((todo) => (
        <TodoItem todo={todo} />
      ))}
    </ul>
  )
}

export default TodoList

TodoItem.tsx

import useTodoStore, { TodoValue } from '@store/useTodoStore'
import { ChangeEvent } from 'react'

interface TodoItemProps {
  todo: TodoValue
}

const TodoItem = ({ todo }: TodoItemProps) => {
  const deleteTodo = useTodoStore((state) => state.deleteTodo)
  const completeTodo = useTodoStore((state) => state.completeTodo)

  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    completeTodo(todo.id, e.target.checked)
  }

  return (
    <li style={{ display: 'flex', marginTop: '10px' }}>
      <label
        id="isComplete"
        style={{
          marginRight: '6px',
          textDecoration: todo.isComplete ? 'line-through' : 'unset'
        }}
      >
        <input
          id="isComplete"
          type="checkbox"
          style={{ marginRight: '6px' }}
          onChange={handleChange}
        />
        {todo.text}
      </label>
      <button
        onClick={() => deleteTodo(todo.id)}
        style={{ marginRight: '6px' }}
      >
        삭제
      </button>
    </li>
  )
}

export default TodoItem

정말 간단하다. store에 정의만 잘하면 그냥 가져와서 사용하기만 하면 된다.

Persist

그리고 redux 처럼 persist를 활용할 수 있다. 간단하게 로컬스토리지나 세션스토리지에 저장해서 활용할 값들을 persist로 정의해서 손쉽게 사용이 가능하다.

useTodoStore.ts

기존에 create로 생성한 부분을 persist로 감싸주기만 하면 된다.

import { persist } from 'zustand/middleware'

const useTodoStore = create(
  persist<TodoProps>(
    (set) => ({
      todos: [],
      addTodo: (todoText) =>
        set((state) => ({
          todos: [
            ...state.todos,
            {
              id: uuid(),
              text: todoText,
              isComplete: false
            }
          ]
        })),
      completeTodo: (todoId, checked) =>
        set((state) => ({
          todos: state.todos.map((todo) =>
            todo.id === todoId ? { ...todo, isComplete: checked } : todo
          )
        })),
      deleteTodo: (todoId) =>
        set((state) => ({
          todos: state.todos.filter((todo) => todo.id !== todoId)
        }))
    }),
    {
      name: 'todo-key',
      getStorage: () => localStorage
    }
  )
)

export default useTodoStore

Storage Clear

스토리지의 값을 clear 하고 싶다면 clearStorage를 호출하면 된다.

useTodoStore.persist.clearStorage()

DevTools

devTool도 활용할 수 있는데 리덕스 DevTools를 활용하는 것과 똑같다. 아래와 같이 devtools도 create로 정의한 부분에서 감싸주면 된다.

import { v4 as uuid } from 'uuid'
import { create } from 'zustand'
import { persist, devtools } from 'zustand/middleware'

const useTodoStore = create(
  persist(
    devtools<TodoProps>((set) => ({
      todos: [],
      addTodo: (todoText) =>
        set((state) => ({
          todos: [
            ...state.todos,
            {
              id: uuid(),
              text: todoText,
              isComplete: false
            }
          ]
        })),
      completeTodo: (todoId, checked) =>
        set((state) => ({
          todos: state.todos.map((todo) =>
            todo.id === todoId ? { ...todo, isComplete: checked } : todo
          )
        })),
      deleteTodo: (todoId) =>
        set((state) => ({
          todos: state.todos.filter((todo) => todo.id !== todoId)
        }))
    })),
    {
      name: 'todo-key',
      getStorage: () => localStorage
    }
  )
)

export default useTodoStore

persist, devtools 를 적용하면서 store 분리시켜서 관리

위와 같이 정의하는 부분이 헷갈린다면, 아래와 같이 분리해서 persist와 devtools를 적용시켜도 된다.

import { v4 as uuid } from 'uuid'
import { create, StateCreator } from 'zustand'
import { persist, devtools } from 'zustand/middleware'

export interface TodoValue {
  id: string
  text: string
  isComplete: boolean
}

export interface TodoProps {
  todos: TodoValue[]
  addTodo: (todoText: string) => void
  completeTodo: (id: string, checked: boolean) => void
  deleteTodo: (id: string) => void
}

const createStore: StateCreator<TodoProps> = (set) => ({
  todos: [],
  addTodo: (todoText: string) =>
    set((state: TodoProps) => ({
      todos: [...state.todos, { id: uuid(), text: todoText, isComplete: false }]
    })),
  completeTodo: (id: string, checked: boolean) =>
    set((state: TodoProps) => ({
      todos: state.todos.map((todo) =>
        todo.id === id ? { ...todo, isComplete: checked } : todo
      )
    })),
  deleteTodo: (id: string) =>
    set((state: TodoProps) => ({
      todos: state.todos.filter((todo) => todo.id !== id)
    }))
})

const useTodoStore = create(
  devtools(
    persist<TodoProps>(createStore, {
      name: 'todo-store',
      getStorage: () => localStorage
    })
  )
)

export default useTodoStore

async 요청 보내기

비동기 요청도 처리하는 것이 쉽다. redux의 경우에는 redux-toolkit의 경우는 비동기 처리가 그냥 redux로 thunk로 처리하는 것과는 달리 createSlice로 쉽게 처리가 가능했다. 그런데 zustand의 경우도 그냥 사용하면 되기 때문에 너무나 편리했다. 아래와 같이 그냥 fetch를 사용하는 그대로 정의해서 사용하고 불러와서 활용하면 된다.

import { create, StateCreator } from 'zustand'
import { devtools, persist } from 'zustand/middleware'

interface UserProps {
  user: User | null
  fetch: (id: number) => Promise<void>
}

const createStore: StateCreator<UserProps> = (set) => ({
  user: null,
  fetch: async (id: number) => {
    const path = `https://jsonplaceholder.typicode.com/users/${id}`
    const response = await fetch(path)
    set({ user: await response.json() })
  }
})

const useUserStore = create(
  devtools(
    persist(createStore, {
      name: 'user',
      getStorage: () => sessionStorage
    })
  )
)

export default useUserStore
import useUserStore from '@store/useUserStore'
import { useEffect } from 'react'

const User = () => {
  const { fetch, user } = useUserStore((state) => state)

  useEffect(() => {
    fetch(1)
  }, [fetch])

  return (
    <section>
      <div>
        <span style={{ marginRight: '16px' }}>{user?.id}</span>
        <span>{user?.name}</span>
      </div>
      <div>{user?.email}</div>
      <div>{user?.phone}</div>
      <div>{user?.website}</div>
    </section>
  )
}

export default User

2. What I did?

프로젝트

AuthHeader 서버 컴포넌트로 변경하기

import AuthDivider from '@/components/(auth)/AuthDivider'
import AuthHeader from '@/components/(auth)/AuthHeader'
import AuthPage from '@/components/(auth)/AuthPage'
import OAuthFooter from '@/components/(auth)/OAuthFooter'
import RegisterForm from '@/components/(auth)/RegisterForm'
import RegisterFormView from '@/components/(auth)/RegisterFormView'

const Register = () => {
  return (
    <AuthPage
      header={<AuthHeader />}
      divider={<AuthDivider />}
      footer={<OAuthFooter />}
    >
      <span className="text-xl xs:text-[10px]">단계 1/2</span>
      <RegisterForm>
        <RegisterFormView />
      </RegisterForm>
    </AuthPage>
  )
}

export default Register

AuthHeader는 현재 클라이언트 컴포넌트다.

'use client'

const AuthHeader = () => {
  const router = useRouter()

  return (
    <>
      <Image
        src={BridgeLogo}
        alt="로고 이미지"
        className="w-[19rem] h-[7rem] cursor-pointer"
        onClick={() => router.push('/')}
      />
    </>
  )
}

export default AuthHeader

처음에 아무생각 없이 router를 사용하고 클라이언트 컴포넌트를 사용했다. Next의 Link를 사용하면 서버 컴포넌트로 만들면서 로고 이미지를 클릭했을 때 루트 페이지로 보낼 수 있다.

const AuthHeader = () => {
  return (
    <Link href="/">
      <Image
        src={BridgeLogo}
        alt="로고 이미지"
        className="w-[19rem] h-[7rem] cursor-pointer"
      />
    </Link>
  )
}

export default AuthHeader

이렇게 최대한 서버 컴포넌트로 바꾸려고 하는 이유는 처음에 렌더링될 때 최대한 사용자가 많은 부분을 볼 수 있게 하기 위함이였다. 하지만 지금 이런 노력이 크게 의미가 없는 상황이다. 로고 이미지를 클라이언트(프론트)에서 가지고 있는 상태이기 때문에 이미지를 받아오려면 대부분의 js 파일을 받아온다음에 이미지를 받아온다. 그래서 결국 사용자가 조금이라도 빠르게 보여주려고 하자는 의도로 서버 컴포넌트로 사용하려고 했지만 지금 같은 상황에서는 그게 크게 의미가 없어진다고 생각한다. 대신 만약 로고 이미지를 데이터베이스에 저장해놓고 처음에 HTML을 서버에서 만들어서 보내줄 때 이미지를 먼저 가지고 올 수 있다면 내가 시도했던 방법은 효과를 볼 것이다.

profile
데이터리터러시를 중요하게 생각하는 프론트엔드 개발자

0개의 댓글