-
Notifications
You must be signed in to change notification settings - Fork 7
베이스 토스트 만들기
토스트 개발 이전에는 사용자의 동작에 대한 성공/실패 여부를 유저가 알도록 하는 방법으로
성공/실패 상태를 컴포넌트에 저장하고 해당 상태에 따라 에러메시지 혹은 성공메시지가 출력되는 방식을 사용하였습니다.
이 방법에서 두 가지 문제점을 찾을 수 있었습니다.
- 사용자의 행동에 대한 반응이 눈에 띄게 보이지 않아 에러가 발생한 경우 알아차리기 어렵다.
- API 호출이 있는 모든 모달 컴포넌트에게 요청 성공/실패, 실패 원인 등이 저장되는 상태를 갖고 있도록 해야 한다.
이러한 문제점을 해결하기 위하여, 사용자의 행동에 대한 반응이 눈에 띄게 보이면서
상태 저장 없이 바로 메시지를 보여줄 수 있는 토스트를 사용하기로 결정내렸습니다.
어떤 식으로 토스트를 만들면 조원들 모두가 편하게 호출하여 사용할 수 있을지 고민하다가,
이전부터 관심있던 모달/토스트 라이브러리 sweetalert가 생각났습니다.
Toast.fire({
icon: 'success',
title: 'Signed in successfully'
})
위의 코드는 sweetalert의 토스트 호출 방식인데요,
이처럼 .fire({옵션들})
을 호출하면 손쉽게 이용할 수 있는 토스트를 구현하기로 마음먹었습니다.
주의 : 이 토스트는 Redux에 의존성을 가지고 있는 토스트입니다. 다른 상태관리 툴을 사용하시는 분들, 상태관리 툴을 사용하지 않으시는 분들께서는 해당 글을 참고하셔서 코드를 상황에 맞게 변형하셔야 합니다.
https://github.com/boostcampwm-2021/web09-Duxcord/pull/308
토스트가 기본적으로 가지고 있는 값은
- 표시할 메시지
- 종류 (성공/실패)
- 지속 기간
이며, 추가적으로 토스트를 삭제할 때 토스트를 식별할 수 있도록 id 값을 부여하여 인터페이스를 구성했습니다.
interface ToastData {
message: string;
type: 'success' | 'warning';
duration?: number;
id?: string;
}
리덕스 스토어에서 전역 상태로 가져야 할 토스트의 목록을 관리하는 slice를 생성했습니다.
- 초기값으로는 빈 ToastData Array를 갖고
- (1) 토스트 추가 (2) 토스트 삭제 두 가지의 동작을 수행할 수 있는
슬라이스를 정의해두었습니다.
import { ToastData } from '@customTypes/toast';
import { createSlice } from '@reduxjs/toolkit';
const initState = Array<ToastData>();
const { reducer: toastReducer, actions } = createSlice({
name: 'toast',
initialState: initState,
reducers: {
addToast: (state, { payload }) => [...state, payload],
popToast: (state, { payload }) => state.filter((toast) => toast.id !== payload.id),
},
});
export const { addToast, popToast } = actions;
export default toastReducer;
호출 시 토스트를 추가하고 duration 이후 토스트를 삭제하는 함수를 반환하는 훅을 구현했습니다.
export const useToast = () => {
const dispatch = useDispatch();
const encodeBase64 = (str: string): string => Buffer.from(str, 'binary').toString('base64');
const fireToast = ({ message, type, duration = 2000 }: ToastData) => {
const id = encodeBase64(String(new Date().getTime()).slice(-6));
dispatch(addToast({ message, type, duration, id }));
setTimeout(() => dispatch(popToast({ id })), duration + 600);
};
return { fireToast };
};
600을 더해준 이유는 토스트가 사라지는 애니메이션이 보일 시간을 확보하기 위함입니다.
전역으로 선언된 Toast 컴포넌트의 경우 리덕스 스토어의 toast 목록을 참조하여 토스트들을 화면에 보여줍니다.
const useToasts = () => useSelector((state: RootState) => state.toast);
export default function Toast() {
const toasts = useToasts();
return (
<Wrapper>
{toasts.map((toast) => (
<ToastItem ... />
))}
</Wrapper>
);
}
ToastItem의 경우 상태에 따라 다른 애니메이션이 보이도록 구현했습니다.
export default function ToastItem({ message, type, duration }: ToastData) {
const [isClosing, setIsClosing] = useState(false);
useEffect(() => {
const setExitTimeout = setTimeout(() => {
setIsClosing(true);
clearTimeout(setExitTimeout);
}, duration!);
return () => clearTimeout(setExitTimeout);
}, []);
return (
<Wrapper isClosing={isClosing}>
<InnerWrapper color={type}>
<div>{message}</div>
</InnerWrapper>
<Line type={type} duration={duration! / 1000} />
</Wrapper>
);
}
const Wrapper = styled.div`
...
animation: 0.3s forwards ${(props) => (props.isClosing ? 'slideToTop' : 'slideFromRight')};
...
`
const Line = styled.div`
...
animation: ${(props) => props.duration}s linear timer;
...
다음과 같이 useToast 훅과 fireToast 함수를 사용할 수 있습니다.
// import
import { useToast } from '@hooks/index';
// 초기화
const { fireToast } = useToast();
// 사용하기
fireToast({ message: TOAST_MESSAGE.___, type: 'success' });