영상 설명: 빈 댓글을 입력하면 경고창이, 댓글 삭제에 성공하면 성공했다는 알림이 뜬다.
처음에는 알림창을 각각의 컴포넌트에서 호출하고 그에 따른 상태 관리도 따로 했었다. 이 방법에는 다음과 같은 단점이 있었다.
- 알림창을 사용하는 컴포넌트마다
<Notice/>
를 호출하고useState()
로 상태관리를 해줘야 하며 알림창을 띄우는 함수를 따로 구현해야 해서 코드가 상당히 복잡해진다.- 특정 컴포넌트에서 호출하기 때문에 해당 컴포넌트가 반환되면 알림창도 같이 꺼진다. (가장 큰 문제)
알림창을 사용할 때마다 아래의 세팅을 해줘야 한다.
// 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
에서 관리하기로 결심했다.
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에 모두 담아준다. 이제 알림창을 사용할 때마다 여기 작성해둔 함수를 갖다 쓰기만 하면 된다.
위에서 작성한 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를 사용한다.
<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로 옮겨야 할 듯하다.