[React] Context API 를 이용해서 모달창 띄우기 (typescript)

코린·2023년 8월 31일
0

리액트

목록 보기
12/22

주의!

이 예제는 하나의 모달창을 모든 페이지에서 띄우는 것 입니다!
배열에 여러 모달창을 넣고 띄우는 것은 아닙니다!

TypeScript 와 Context API 활용하기
를 참고해서 작성했습니다!

사용 기술

  • React
  • Next
  • Typescript
  • Material UI
  • Context API
  • useReducer

💬 모달창 띄우기

✔️ Context 작성

Context 파일 생성

폴더구조

[src]
 -[app]
 	-[nextPage]
    	- [join]
        	-page.tsx
 -[components]
 	- modal.tsx
 -[context]
 	- modalContext.tsx
 - page.tsx
 - layout.tsx
 

modalContext.tsx
(Provider와 Context 파일을 따로 분리해도 되지만 저는 합쳐서 사용했습니다.)

  1. 타입을 선언해줍니다.

    
    /* 타입선언 */
    
    //모달 창 유무
    type ModalState = { isModal: boolean;}
    
    const initialState: ModalState = {
      isModal: false,
    }
    
    //모든 액션들을 위한 타입
    type ModalAction =
      | { type: 'SHOW_MODAL' }
      | { type: 'HIDE_MODAL' }
    
    //디스패치를 위한 타입 (Dispatch를 리액트에서 불러올 수 있음)
    //액션들의 타입을 Dispatch의 Generics로 설정
    type ModalDispatch = Dispatch<ModalAction>
    

    isModaltrue일 경우 모달창을 띄워주고
    isModalfalse일 경우 모달창을 없애줍니다.

  2. Context를 생성합니다.

state 와 dispatch에 대해서 따로따로 생성해주었습니다.

/* Context */
//Context 만들기
const ModalStateContext = createContext<ModalState | null>(null)
const ModalDispatchContext = createContext<ModalDispatch | null>(null)

쉽게 이해해보자면! state는 상태값을 사용할 수 있게 해주고, dispatch는 상태값을 변경할 수 있게 해줍니다!

  1. 리듀서 함수 작성
/* Reducer */
//리듀서
function reducer(state: ModalState, action: ModalAction): ModalState {
  switch (action.type) {
    case 'SHOW_MODAL':
      return {
        isModal: true,
      }
    case 'HIDE_MODAL':
      return {
        isModal: true,
      }
    default:
      return state
  }
}

리듀서 함수를 통해서 isModal의 상태값을 변경할 수 있게 되었습니다.

Provider 작성

Provider 파일을 따로 작성해도 되지만 저는 위 파일에 이어서 작성했습니다!

  1. ModalProvider 생성
/* Provider */
//Provider에서 useReducer를 사용하고
//ModalStateContext.Provider 와 ModalDispatchContext.Provider로 children을 감싸서 반환합니다.
//기존에 state와 dispatch를 넣어서 반환하는 것과 동일한 것 같음

export function ModalProvider({ children }: { children: React.ReactNode }) {
  const [state, dispatch] = useReducer(reducer, initialState)

  //여기 children에는 하위 컴포넌트들이 다 들어갈 것!
  //children -> 리액트에서 자동으로 주입해주는 예약어 개념

  return (
    <ModalStateContext.Provider value={state}>
      <ModalDispatchContext.Provider value={dispatch}>
        {children}
      </ModalDispatchContext.Provider>
    </ModalStateContext.Provider>
  )
}

state 와 dispatch에 대한 provider를 모두 감싸주었습니다.
children은 하위 컴포넌트들이라고 생각하면 됩니다.

이 ModalProvider를 이용해서 ModalContext 상태를 이용할 범위를 지정해 줄 것이기 때문에 꼭 export 해주어야 합니다!

  1. 커스텀 Hook 작성
/* custom Hook */
// state 와 dispatch를 쉽게 사용하기 위한 커스텀 Hook

export function useModalState() {
  const state = useContext(ModalStateContext)

  if (!state) throw new Error('useModalState: Cannot find ModalProvider')
  return state
}

export function useModalDispatch() {
  const dispatch = useContext(ModalDispatchContext)

  if (!dispatch) throw new Error('useModalDispatch: Cannot find ModalProvider')
  return dispatch
}

각각 state , dispatch를 쉽게 사용하기 위해서 커스텀 훅을 작성했습니다.

modalContext.tsx 전체코드

'use client'
import React, { useReducer, useContext, createContext, Dispatch } from 'react'

/* 타입선언 */

//모달 창 유무
type ModalState = { isModal: boolean }

const initialState: ModalState = {
  isModal: false,
}

//모든 액션들을 위한 타입
// 순서대로 1: 공란이 하나라도 있을 경우 2: 이메일 틀렸을 경우 3: 패스워드 틀렸을 경우
type ModalAction =
  | { type: 'SHOW_MODAL' }
  | { type: 'HIDE_MODAL' }

//디스패치를 위한 타입 (Dispatch를 리액트에서 불러올 수 있음)
//액션들의 타입을 Dispatch의 Generics로 설정
type ModalDispatch = Dispatch<ModalAction>

/* Context */
//Context 만들기
const ModalStateContext = createContext<ModalState | null>(null)
const ModalDispatchContext = createContext<ModalDispatch | null>(null)

/* Reducer */
//리듀서
function reducer(state: ModalState, action: ModalAction): ModalState {
  switch (action.type) {
    case 'SHOW_MODAL':
      return {
        isModal: true
      }
    case 'HIDE_MODAL':
      return {
        isModal: true
      }
    default:
      return state
  }
}

/* Provider */
//Provider에서 useReducer를 사용하고
//ModalStateContext.Provider 와 ModalDispatchContext.Provider로 children을 감싸서 반환합니다.
//기존에 state와 dispatch를 넣어서 반환하는 것과 동일한 것 같음

export function ModalProvider({ children }: { children: React.ReactNode }) {
  const [state, dispatch] = useReducer(reducer, initialState)

  //여기 children에는 하위 컴포넌트들이 다 들어갈 것!
  //children -> 리액트에서 자동으로 주입해주는 예약어 개념

  return (
    <ModalStateContext.Provider value={state}>
      <ModalDispatchContext.Provider value={dispatch}>
        {children}
      </ModalDispatchContext.Provider>
    </ModalStateContext.Provider>
  )
}

/* custom Hook */
// state 와 dispatch를 쉽게 사용하기 위한 커스텀 Hook

export function useModalState() {
  const state = useContext(ModalStateContext)

  if (!state) throw new Error('useModalState: Cannot find ModalProvider')
  return state
}

export function useModalDispatch() {
  const dispatch = useContext(ModalDispatchContext)

  if (!dispatch) throw new Error('useModalDispatch: Cannot find ModalProvider')
  return dispatch
}

✔️ 모달 창 생성

모달 컴포넌트 생성

modal.tsx

  1. 모달 구조만 작성
'use client'

import Box from '@mui/material/Box'
import Button from '@mui/material/Button'

export default function Modal() {

  const router = useRouter()

  const onClick = () => {
	//모달 버튼
  }

  return (
    <Box
      sx={{
        width: '100%',
        height: '100%',
        backgroundColor: 'rgba(40, 40, 40, .1) ',
        display: 'flex',
        flexDirection: 'column',
        justifyContent: 'center',
        alignItems: 'center',
        position: 'fixed',
      }}
    >
      <Box
        sx={{
          width: '300px',
          height: '300px',
          backgroundColor: 'white',
          display: 'flex',
          flexDirection: 'column',
          jjustifyContent: 'center',
          alignItems: 'center',
        }}
      >
        <Box sx={{ color: 'black', fontSize: '30px', margin: '10px' }}>
          제목
        </Box>
        <Box sx={{ color: 'black', fontSize: '20px', margin: '10px' }}>
         설명
        </Box>
        <Button onClick={onClick}>확인</Button>
      </Box>
    </Box>
  )
}
  1. context API를 사용해서 모달 상태를 가져오기

앞선 부분에서 만들었던 Hook을 이용해서 모달의 상태를 가져오고 변경하는 코드를 작성해보겠습니다.

//...생략...//

//필요한 훅을 import
import { useModalState
        , useModalDispatch } from '@/context/modalContext'

export default function Modal() {
  //훅을 불러옵니다.
  const dispatch = useModalDispatch()
  const state = useModalState()

  //...생략...//

이렇게 선언한 statedispatch 를 이용하면 됩니다!

state는 state.isModal 과 같이 변수 값을 가져오면 됩니다.
dispatch 는 dispatch({ type: 'SHOW_MODAL' }) 과 같이 상태 값을 액션함수, 리듀서를 통해서 변경해주면 됩니다.

'use client'

import Box from '@mui/material/Box'
import Button from '@mui/material/Button'

import { useModalDispatch } from '@/context/modalContext'

export default function Modal() {
  const dispatch = useModalDispatch()

  const onClick = () => {
    dispatch({ type: 'HIDE_MODAL' })
  }

  return (
    <Box
      sx={{
        width: '100%',
        height: '100%',
        backgroundColor: 'rgba(40, 40, 40, .1) ',
        display: 'flex',
        flexDirection: 'column',
        justifyContent: 'center',
        alignItems: 'center',
        position: 'fixed',
      }}
    >
      <Box
        sx={{
          width: '300px',
          height: '300px',
          backgroundColor: 'white',
          display: 'flex',
          flexDirection: 'column',
          jjustifyContent: 'center',
          alignItems: 'center',
        }}
      >
        <Box sx={{ color: 'black', fontSize: '30px', margin: '10px' }}>
          모달~
        </Box>
        <Button onClick={onClick}>확인</Button>
      </Box>
    </Box>
  )
}

모달창에서는 모달창의 유무만 결정지으면 되므로 dispatch 만 이용하도록 하겠습니다.

Context 파일에 모달창 추가

modalContext.tsx

Provider 부분에 모달창을 추가해줍니다.

/* Provider */
//Provider에서 useReducer를 사용하고
//ModalStateContext.Provider 와 ModalDispatchContext.Provider로 children을 감싸서 반환합니다.
//기존에 state와 dispatch를 넣어서 반환하는 것과 동일한 것 같음

export function ModalProvider({ children }: { children: React.ReactNode }) {
  const [state, dispatch] = useReducer(reducer, initialState)

  //여기 children에는 하위 컴포넌트들이 다 들어갈 것!
  //children -> 리액트에서 자동으로 주입해주는 예약어 개념

  return (
    <ModalStateContext.Provider value={state}>
      <ModalDispatchContext.Provider value={dispatch}>
        {children}
        {state.isModal && <Modal />}
      </ModalDispatchContext.Provider>
    </ModalStateContext.Provider>
  )
}

isModaltrue일 경우엔 모달창이 뜰 것이고
isModalfalse일 경우엔 모달창이 뜨지 않을 것 입니다.

✔️ 원하는 영역 Provider로 묶기

원하는 영역 지정

저는 모든 파일에서 접근 가능하게 만들고 싶어서 layout.tsx 에 추가해 주었습니다.

layout.tsx

import './globals.css'
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import { ModalProvider } from '@/context/modalContext'
import Box from '@mui/material/Box'

const inter = Inter({ subsets: ['latin'] })

export const metadata: Metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <ModalProvider>
        <body>
          <Box
            sx={{
              width: 300,
              height: 300,
              flexDirection: 'column',
              display: 'flex',
              gap: '10px',
              padding: '10px',
            }}
          >
            {children}
          </Box>
        </body>
      </ModalProvider>
    </html>
  )
}

ModalProvider 의 영역에서만 상태값에 접근 가능할까?
답: 아니요!
모든 파일에서 접근가능합니다.
하지만 읽는 데이터 값이 다르게 됩니다.
감싸진 영역 : 초기값
그 이외 영역 : default 값

✔️ 모달창 띄우기

저는 회원가입 창에서 띄우도록 작성했습니다!

nextPage/join/page.tsx

'use client'
import React, { useState, useRef, ChangeEvent, useContext } from 'react'
import TextField from '@mui/material/TextField'
import Button from '@mui/material/Button'
import Box from '@mui/material/Box'
import Link from 'next/link'
//모달 커스텀 hook
import { useModalDispatch } from '@/context/modalContext'

interface FormValues {
  name: string
  email: string
  password: string
}

export default function Join() {
  /* 모달 상태 전역관리 */
  const dispatch = useModalDispatch()

  const [form, setForm] = useState<FormValues>({
    name: '',
    email: '',
    password: '',
  })

  const { name, email, password } = form

  const onChange = (e: ChangeEvent<HTMLInputElement>) => {
    const { value, name } = e.target

    setForm({
      ...form,
      [name]: value,
    })
  }

  const passwordRef = useRef<HTMLInputElement | null>(null)
  const emailRef = useRef<HTMLInputElement | null>(null)

  const onClick = () => {
    if (validation(1) && validation(2)) {
      if (password === '' || email === '' || name === '')
        return dispatch({ type: 'SHOW_MODAL' })
      
    } else {
      if (!validation(2)) {
        emailRef.current?.focus()
        console.log('이메일 틀림')
        dispatch({ type: 'SHOW_MODAL' })
      } else if (!validation(1)) {
        passwordRef.current?.focus()
        console.log('비밀번호 틀림')
        dispatch({ type: 'SHOW_MODAL' })
      }
    }
  }

  //비밀번호, 이메일 유효성
  const validation = (type: number) => {
    const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*[\d\W_]).{8,}$/
    const emailRegex = /.+@.+/

    switch (type) {
      case 1:
        if (password === '') return true
        return passwordRegex.test(password)
      case 2:
        if (email === '') return true
        return emailRegex.test(email)
    }
  }

  return (
    <>
      <TextField
        id="outlined-basic"
        label="이름"
        variant="outlined"
        value={name}
        name="name"
        onChange={onChange}
      />
      <TextField
        id="outlined-basic"
        label="이메일"
        variant="outlined"
        value={email}
        name="email"
        onChange={onChange}
        inputRef={emailRef}
        error={!validation(2)}
        helperText={validation(2) ? '' : '잘못된 이메일 형식 입니다.'}
      />
      <TextField
        id="outlined-basic"
        label="패스워드"
        type="Password"
        variant="outlined"
        value={password}
        name="password"
        onChange={onChange}
        inputRef={passwordRef}
        error={!validation(1)}
        helperText={
          validation(1)
            ? ''
            : '비밀번호는 8자 이상, 특수 문자 1개 이상, 영문 소문자 최소 1개, 영문 대문자 최소 1개의 조건을 만족해야 합니다.'
        }
      />
      <Button variant="outlined" onClick={onClick}>
        가입하기
      </Button>
      <Link href="/">
        <Button variant="outlined">HOME</Button>
      </Link>
    </>
  )
}

(제가 쓴 코드는 다른내용이 좀 더 추가되어 있어서 결과차이 조금 다르게 뜹니다!)

이런식으로 모달창이 뜨게 됩니다!!!!!
제가 올려둔 참고블로그 꼭꼭꼭꼭 보세요,,,,,,,,,,,,,,,,,,,

그럼...
20000,,,

profile
안녕하세요 코린입니다!

0개의 댓글