Toast 알림을 구현하면서 어디서든 간편하게 사용할 수 있도록 하기 위해 전역 상태관리가 필요했다.
요즘 Zustand가 핫한 거 같아서 사용해 봤다! (+ 곰이 귀엽기도 하고 ㅎㅎ)
추가로 나는 Next.js 14 App router를 사용 중이다.
Zustand
다른 상태관리 툴과 비교했을 때 장점은
- Context API와 달리 상태 변경 시 불필요한 리렌더링이 일어나지 않는다.
- 동작을 이해하기 위해 알아야 하는 코드 양이 적다.
- 한 개의 중앙에 집중된 형식의 스토어 구조를 활용하면서 상태를 정의하고 사용하는 방법이 단순하다.
설치
# npm
npm install zustand
# yarn
yarn add zustand
전역상태관리를 통해 Toast 알림 만들기
store 생성
toast 알림은 여러 개가 나타날 수 있기 때문에 배열(toastList
)로 만들었다.
store/toast.ts
import { ToastItemType } from '@/types'
import { create } from 'zustand'
interface ToastState {
toastList: ToastItemType[] // Toast 알림 List
addToastList: (elem: ToastItemType) => void // Toast 알림을 추가하는 함수
subtractToastList: (id: string) => void // Toast 알림을 제거하는 함수
}
export const useToastStore = create<ToastState>((set) => ({
toastList: [],
addToastList: (toast: ToastItemType) =>
set((state) => ({ toastList: [...state.toastList, toast] })),
subtractToastList: (id: string) =>
set((state) => ({
toastList: state.toastList.filter((toast) => toast.id !== id),
})),
}))
관련 Type은 아래와 같다.
성공, 경고, 에러의 경우를 나눠 ToastType
을 설정하고
ToastItemType
에는 고유한 id, ToastType(성공, 경고, 에러), 전달할 message를 담았다.
export type ToastType = 'success' | 'warning' | 'error'
export type ToastItemType = {
id: string
type: ToastType
message: string
}
ToastList component
전역 상태에서 toastList
를 가져와 각각의 요소를 반복을 돌렸다.
클라이언트에서만 실행되도록 하고 React portal을 통해서 상위 요소인 #toast-root
에서 나타나도록 했다.
'use client'
import { useToastStore } from '@/store/toast'
import React from 'react'
import ReactDOM from 'react-dom'
import styled from 'styled-components'
import ToastItem from './ToastItem'
function ToastList() {
const { toastList } = useToastStore() // 전역 상태에서 toastList를 가져옴
if (typeof document === 'undefined' || toastList.length === 0) return null // 클라이언트에서만 실행되도록
const element = document.getElementById('toast-root')
if (!element) return null
return ReactDOM.createPortal(
<ToastListWrapper>
{toastList.map((toast) => (
<ToastItem key={toast.id} toast={toast} />
))}
</ToastListWrapper>,
element,
)
}
const ToastListWrapper = styled.div`
position: fixed;
top: 20px;
right: 20px;
z-index: 99;
display: flex;
flex-direction: column;
gap: 12px;
width: 100%;
max-width: 320px;
overflow: hidden;
`
export default ToastList
#toast-root
div를 추가해 해당 요소 아래에 Toast 알림이 생성되도록 했다.
app/layout.tsx
import CoreProvider from '@/providers/CoreProvider'
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang="ko">
<head />
<body>
<CoreProvider>{children}</CoreProvider>
<div id="modal-root" />
<div id="toast-root" /> // 여기 추가!
</body>
</html>
)
}
CoreProvider 안에서 ToastList component를 불러올 수 있도록 했다.
import ToastList from '@/components/ToastList'
import ReactQueryProvider from './ReactQueryProvider'
import StyledComponentProvider from './StyledComponentProvider'
function CoreProvider({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<ReactQueryProvider>
<StyledComponentProvider>
<ToastList /> // ToastList component 추가
{children}
</StyledComponentProvider>
</ReactQueryProvider>
)
}
export default CoreProvider
ToastItem component
4000ms 후 visible
을 false
로 설정해 Toast 알림이 점점 사라지는 효과를 줬다.
애니메이션 효과를 위해 바로 제거하지 않고 타이머를 설정해 500ms의 간격을 두고 전역상태에서 제거했다.
사라질 때는 clearTimeout
으로 설정된 타이머를 제거해 불필요한 코드 실행을 방지했다.
각각의 타입에 따라 다른 색으로 강조를 해줬다.
'use client'
import { useToastStore } from '@/store/toast'
import { ToastItemType, ToastType } from '@/types'
import React, { useEffect, useState } from 'react'
import styled, { keyframes } from 'styled-components'
const DURATION = 4000
const ANIMATION = 500
interface ToastItemProps {
toast: ToastItemType
}
function ToastItem({ toast }: ToastItemProps) {
const [visible, setVisible] = useState(true)
const { subtractToastList } = useToastStore()
const { id, type, message } = toast
useEffect(() => {
const timer = setTimeout(() => {
setVisible(false)
}, DURATION)
const removeTimer = setTimeout(() => {
subtractToastList(id) // toastList에서 제거
}, DURATION + ANIMATION)
return () => {
clearTimeout(timer)
clearTimeout(removeTimer)
}
}, [id, subtractToastList])
return (
<>
<ToastBox id={id} $type={type} $visible={visible}>
{message}
</ToastBox>
</>
)
}
const getToastColors = (type: ToastType) => {
switch (type) {
case 'success':
return { backgroundColor: '#E1F4EB', borderColor: '#64B78E' }
case 'warning':
return { backgroundColor: '#FCF4DE', borderColor: '#E7B416' }
case 'error':
return { backgroundColor: '#FCDDDD', borderColor: '#D9534F' }
default:
return { backgroundColor: '#ffffff', borderColor: '#000000' }
}
}
const fadeIn = keyframes`
from { opacity: 0; }
to { opacity: 1; }
`
const fadeOut = keyframes`
from { opacity: 1; }
to { opacity: 0; }
`
const ToastBox = styled.div<{ $type: ToastType; $visible: boolean }>`
background-color: ${({ $type }) => getToastColors($type).backgroundColor};
border: 1px solid ${({ $type }) => getToastColors($type).borderColor};
color: #000000;
padding: 16px 24px;
border-radius: 16px;
font-size: 14px;
animation: ${({ $visible }) => ($visible ? fadeIn : fadeOut)} 0.5s ease;
transition:
opacity 0.5s ease,
transform 0.5s ease;
`
export default ToastItem
일단 이렇게 하면 준비는 완료다!
Custom hooks
이제 필요한 건 실행인데 실행도 간편하게 구현해 보자. (react-toastify처럼)
어디서든 type과 message를 받아 Toast 알림을 띄우고 싶었다.
import { useToastStore } from '@/store/toast'
import { ToastType } from '@/types'
import { v4 as uuidv4 } from 'uuid'
export default function useToast() {
const { addToastList } = useToastStore()
const toast = (type: ToastType, message: string) => {
const id = uuidv4() // 각각의 고유한 id를 사용
addToastList({ id, type, message }) // toastList에 추가
}
return { toast }
}
이제 사용 시 hooks를 가져와 type과 message만 전달해 주면 사용할 수 있다. 🎉
'use client'
import useToast from '@/hooks/useToast'
import styled from 'styled-components'
export default function ToastTest() {
const { toast } = useToast()
return (
<main>
<StyledButton
type="button"
onClick={() => {
toast('success', '성공적이에요.')
}}
>
success
</StyledButton>
<StyledButton
type="button"
onClick={() => {
toast('warning', '경고에요.')
}}
>
warning
</StyledButton>
<StyledButton
type="button"
onClick={() => {
toast('error', '오류에요.')
}}
>
error
</StyledButton>
</main>
)
}
const StyledButton = styled.button`
background-color: #000000;
color: #fff;
border-radius: 16px;
padding: 4px 8px;
`
완성된 Toast 알림은 아래와 같다.
'IT > React' 카테고리의 다른 글
useState 원리 (+closure) 그리고 useCallback 언제 사용하면 좋을까? (0) | 2024.01.12 |
---|---|
recoil로 모달 전역 상태관리하기(+typescript) (1) | 2024.01.10 |
필터 구현 로직 custom hook으로 만들기(+뒤로가기 시 필터값 유지) (0) | 2023.12.05 |
Pagination custom hook (0) | 2023.11.06 |
[React] Button Component 잘 만드는 방법! (0) | 2023.11.05 |