[Next.Js] useContext로 알림창 구현하기

코딩하는 남자·2022년 7월 21일
0

Next-Js

목록 보기
2/2

영상 설명: 빈 댓글을 입력하면 경고창이, 댓글 삭제에 성공하면 성공했다는 알림이 뜬다.


알림창을 전역 관리하는 목적

처음에는 알림창을 각각의 컴포넌트에서 호출하고 그에 따른 상태 관리도 따로 했었다. 이 방법에는 다음과 같은 단점이 있었다.

  1. 알림창을 사용하는 컴포넌트마다 <Notice/> 를 호출하고 useState() 로 상태관리를 해줘야 하며 알림창을 띄우는 함수를 따로 구현해야 해서 코드가 상당히 복잡해진다.
  2. 특정 컴포넌트에서 호출하기 때문에 해당 컴포넌트가 반환되면 알림창도 같이 꺼진다. (가장 큰 문제)

알림창을 사용할 때마다 아래의 세팅을 해줘야 한다.

// pages/posts/[id].tsx

import React, { useState } from 'react';
import Notice from '../components/notification/notice';

// 최초 useState에 들어가는 값
const initialNoticeState = {
  show: false,
  isSuccessed: false,
  header: '',
  message: '',
};

const PostDetail = () => {
  const [notice, setNotice] = useState(initialNoticeState);
  
  // 알림창을 닫는 함수 (몇초가 지나고 알림창이 스스로 이 함수를 호출한다.)
  function handleNoticeClose() {
    setNotice(initialNoticeState);
  }
  
  // 작업 성공 시 초록색 알림창을 보여준다.
  function showSuccessed(header: string, message: string) {
    setNotice({
      show: true,
      isSuccessed: true,
      header,
      message,
    });
  }
  
  // 작업 실패 시 빨간색 알림창을 보여준다.
  function showFailed(header: string, message: string) {
    setNotice({
      show: false,
      isSuccessed: false,
      header,
      message,
    });
  }
  
  
  // 알림창 띄우기
  function onSuccessed() {
    showSuccessed('Success', '성공했습니다!'); 
  }
  
  function onFailed() {
    showSuccessed('Error', '실패했습니다!'); 
  }
  
  return (
    <div>
      <Notice info={notice}/>
      <button onClick={onSuccessed}>성공</button> 
      <button onClick={onFailed}>실패</button> 
    </div>
  );
};

export default PostDetail;

이러한 이유로 useContext를 활용해 알림창을 최상위 컴포넌트 _app.tsx 에서 관리하기로 결심했다.


Context 만들기

useContext와 useState로 코드를 짰다.

// store/notice-context.tsx

import { createContext, useContext } from 'react';
import React, { useState } from 'react';

// 알림창 Context의 State
interface State {
  show: boolean;
  isSuccessed: boolean;
  header: string;
  message: string;
  successed: (info: Info) => void;
  failed: (info: Info) => void;
  close: () => void;
}

export interface Info {
  header: string;
  message: string;
}

// 최초 useState에 들어가는 값
const defaultState: State = {
  show: false,
  isSuccessed: false,
  header: '',
  message: '',
  successed: (info: Info) => {},
  failed: (info: Info) => {},
  close: () => {},
};

// Provider에 들어갈 value를 생성한다.
const StateContext = createContext(defaultState);


const NoticeProvider: React.FC<{ children: React.ReactNode }> = ({
  children,
}) => {
  const [state, setState] = useState(defaultState);
  
  // 작업 성공 시 초록색 알림창을 띄우는 함수
  const successed = (info: Info) => {
    setState((prev) => ({
      ...prev,
      show: true,
      isSuccessed: true,
      header: info.header,
      message: info.message,
    }));
  };
  
  // 작업 실패 시 빨간색 알림창을 띄우는 함수
  const failed = (info: Info) => {
    setState((prev) => ({
      ...prev,
      show: true,
      isSuccessed: false,
      header: info.header,
      message: info.message,
    }));
  };
  
  // 알림창을 닫는 함수
  const close = () => {
    setState(defaultState);
  };

  const noticeCtx: State = {
    show: state.show,
    isSuccessed: state.isSuccessed,
    header: state.header,
    message: state.message,
    successed,
    failed,
    close,
  };
  
  return (
    <StateContext.Provider value={noticeCtx}>{children}</StateContext.Provider>
  );
};

// 사용하기 편하게 훅으로 만들어준다.
export const useNotice = () => {
  return useContext(StateContext);
};

export default NoticeProvider;

위에서 구현했던 함수들을 Context의 State에 모두 담아준다. 이제 알림창을 사용할 때마다 여기 작성해둔 함수를 갖다 쓰기만 하면 된다.


_App.tsx에 렌더링하기

위에서 작성한 Provider를 최상단 컴포넌트 _app.tsx 에 렌더링한다.
이로써 특정 컴포넌트가 반환되어도 알림창은 꺼지지 않고 유지된다.

// _app.tsx

import type { AppProps } from 'next/app';
import Notice from '../components/notification/notice';
import NoticeProvider from '../store/notice-context';

function App({ Component, pageProps }: AppProps) {
  return (
    <NoticeProvider> 
      <Notice />
      <Component {...pageProps} />
    </NoticeProvider>
  );
}

export default App;

알림창 Context를 모든 컴포넌트에서 사용할 수 있게 Provider로 감싸준다.
그리고 알림창 <Notice/> 를 최상단에서 관리한다.


Context 사용하기

이제 위에서 만든 Context를 사용한다.
<Notice/> (알림창) 컴포넌트 내에서는 Context의 show, isSuccessed, header, message, close() 의 값을 받아오고 알림창을 띄울 컴포넌트에서는 successed(), failed() 함수를 실행한다.


먼저 알림창 컴포넌트이다. 나타나고 사라질 때 보이는 효과는 생략했다.

// components/notice.tsx

import React, { useEffect } from 'react';
import { useNotice } from '../store/notice-context';
import styles from './notice.module.scss';

const Notice: React.FC = () => {
  
  // Context 사용
  const { show, isSuccessed, header, message, close } = useNotice(); 

  // 3초뒤에 알림창을 스스로 닫는다.
  useEffect(() => {
    if (show) {
      setTimeout(() => {
        close();
      }, 3000);
    }
  }, [close, show]);
  
  return (
    <div>
      {show && (
        <div className={ isSuccessed ? styles.successed : styles.failed }> 
          <div>
            <div>{header}</div>
            <button onClick={close}></button>
          </div>
          <div>{message}</div>
        </div>
      )}
    </div>
  );
};

export default Notice;

Context 파일에서 정의한 useNotice 훅으로 쉽게 useContext를 호출할 수 있다.

이제 알림창을 띄우는 것도 쉽게 구현 가능하다.

// pages/posts/[id].tsx

import React from 'react';
import { useNotice } from '../store/notice-context';

const PostDetail = () => {
    
  // Context 사용
  const { successed, failed } = useNotice();

  // 알림창 띄우기
  function onSuccessed() {
    successed({ header: 'Success', message: '성공했습니다!' });
  }

  function onFailed() {
    failed({ header: 'Error', message: '실패했습니다!' });
  }

  return (
    <div>
      <button onClick={onSuccessed}>성공</button>
      <button onClick={onFailed}>실패</button>
    </div>
  );
};

export default PostDetail;

</Notice> 컴포넌트를 직접 렌더링할 필요도 없고 함수를 따로 구현할 필요도 없어서 코드가 훨씬 깔끔해졌다. 맨 처음 코드와 비교해보길 바란다.


마치며

사이트에서 다양한 에러 처리에 알림창을 활용할 수 있다. 이번 기능을 만들면서 useContext에 대해서도 복습할 수 있었던 좋은 기회였다.


P.S.
알림창 외에도 사용자에게 한 번 더 확인하는 <Confirm/> 컴포넌트 등 최상단에서 관리하면 편리해질 것이 많아보인다. 기능 하나를 만들 때마다 _app.tsx 에 Provider로 감싸면 보기에 좋지 않을 것이므로 조만간 redux로 옮겨야 할 듯하다.

profile
"신은 주사위 놀이를 하지 않는다."

0개의 댓글