서버연동 삭제
삭제하는 REST api는 다음과 같다.
- protocol: /api/admin/hero
- method: delete
- query parameter: id
- example) /api/admin/hero/id=1
삭제버튼을 누르면 삭제할것인가를 물어보는 Modal 창을 띄운후 ok를 누르면 서버에 삭제 요청을 보내고 정상적으로 삭제가 되면 자식페이지가 없어지므로 부모페이지로 이동한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
export const Hero = (props) => { console.log('View: ', props); const [is_edit, setIs_edit] = useState(false); const handleEditMode = (e) => { setIs_edit(!is_edit); } const [modal, setModal] = useState(false); const toggle = () => setModal(!modal); const handleDelete = () => { toggle(); api.delete(`/api/admin/hero?id=${props.match.params.id}`) .then(response => { console.log(response.data); props.history.push('/heroes/hero'); // this.props.router.push('/heroes/hero'); 3.0.0+ }); } return ( <> <div className="d-flex justify-content-between align-items-center m-3"> <h3>{ is_edit ? 'Hero Edit' : 'Hero Detail View' }</h3> <div> { is_edit ? <button className="btn btn-info" onClick={handleEditMode}>취소</button> : <button className="btn btn-success" onClick={handleEditMode}>수정</button> } <button className="btn btn-danger ml-3" onClick={toggle}>삭제</button> </div> </div> { is_edit ? <Edit id={props.match.params['id']}/> : <View id={props.match.params['id']} /> } <Modal isOpen={modal} toggle={toggle}> <ModalHeader toggle={toggle}>삭제</ModalHeader> <ModalBody> {} 삭제하시겠습니까? </ModalBody> <ModalFooter> <Button color="primary" onClick={handleDelete}>OK</Button>{' '} <Button color="secondary" onClick={toggle}>Cancel</Button> </ModalFooter> </Modal> </> ); } |
부모 정보 갱신
자식정보는 삭제되었지만 삭제된 정보가 부모 컴포넌트인 heroes에는 반영이 안되어있다. 그러므로 삭제되었다는 것을 부모한테 알려주고 부모는 리프레쉬를 해야 한다.
자식컴포넌트인 Hero.js에서 갱신이 필요하다는 액션을 dispatch하고 부모컴포넌트인 Heroes.js에서 정보를 받고 갱신해야 한다. Publisher – Subscriber 구조에서 자식이 생산자(Publisher)가 되고 부모가 소비자(Subscriber)가 된다.
redux와 react-redux를 설치한다.
1 |
yarn add redux react-redux |
기본적인 리덕스 설정을 한다.
store를 생성한다.
1 2 3 4 5 |
import {createStore} from "redux"; import {allReducers} from "./reducers"; export const store = createStore(allReducers, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()); |
index.js에 store를 Provider에 주입한다.
1 2 3 4 5 6 7 8 |
ReactDOM.render( <React.StrictMode> <Provider store={store}> <Root/> </Provider> </React.StrictMode>, document.getElementById('root') ); |
비어있는 hero 리듀서를 만든다.
1 2 3 |
export const heroReducer = (state = '', action) => { return state } |
index.js에서 모든 리듀서를 합친 allReducers를 만든다.
1 2 3 4 |
import {combineReducers} from "redux"; import {heroReducer} from "./heroes"; export const allReducers = combineReducers({heroReducer}); |
액션과 액션타입을 정의한다.
1 2 3 |
... export const REFRESH_HERO = 'hero/REFRESH_HERO'; |
1 2 3 4 5 6 7 |
... export const refreshHero = () => { return { type: REFRESH_HERO } } |
heroes reducer를 구현한다. 모든 정보는 부모가 갖고 있으므로 자식이 부모에게 특정한 값을 전달할 필요는 없다. 단지 갱신되었다는 정보만 필요할 뿐이다. 그러므로, 초기 데이터를 refresh_count 를 0으로 두고 부모가 이 값에 subscribe를 하고 증가하면 값을 받아서 리프레쉬 하도록 하면 된다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import {REFRESH_HERO} from "../actionTypes"; const heroInitialState = { refresh_count: 0 } export const heroReducer = (state = heroInitialState, action) => { switch (action.type) { case REFRESH_HERO: return { ...state, refresh_count: state.refresh_count + 1 } default: return state; } } |
자식컴포넌트에서 액션을 dispatch하는 publisher 코드를 작성한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
... const dispatch = useDispatch(); ... const handleDelete = () => { toggle(); api.delete(`/api/admin/hero?id=${props.match.params.id}`) .then(response => { console.log(response.data); props.history.push('/heroes/hero'); // this.props.router.push('/heroes/hero'); 3.0.0+ // publish to parent dispatch(refreshHero()); }); } ... |
부모컴포넌트에서 subscribe 한다.
refresh_count를 store로 내려받고 변경시 다시 getHeroes()를 호출하도록 useEffect 의 두번째 파라메터 변수로 추가한다.
1 2 3 4 5 6 7 |
... const refresh_count = useSelector(state => state.heroReducer.refresh_count); useEffect(() => { getHeroes() }, [currentPage, refresh_count]); ... |
Toast or SnackBar
삭제 성공후 삭제되었다는 notification을 띄우기 위해서 오픈소스를 사용한다.
1 |
yarn add react-toast-notifications |
toast 메시지는 모든 컴포넌트에서 사용가능해야 하므로 Root 컴포넌트를 래핑하는 Provider를 제공한다.
ToastProvider 에는 여러가지 옵션들이 있는데 여기서는 위치를 가운데 상단으로 고정하는 placement 속성을 사용하였다.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
... import {ToastProvider} from 'react-toast-notifications'; ReactDOM.render( <React.StrictMode> <Provider store={store}> <ToastProvider placement="top-center"> <Root/> </ToastProvider> </Provider> </React.StrictMode>, document.getElementById('root') ); |
삭제시 toast 메시지를 띄워보자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
... const {addToast} = useToasts(); const handleDelete = () => { toggle(); api.delete(`/api/admin/hero?id=${props.match.params.id}`) .then(response => { console.log(response.data); props.history.push('/heroes/hero'); // this.props.router.push('/heroes/hero'); 3.0.0+ // publish to parent dispatch(refreshHero()); addToast('삭제되었습니다.', { appearance: 'success', autoDismiss: true }); }); } |
등록시에도 마찬가지로 toast를 적용해보자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
... const {addToast} = useToasts(); ... api.post('/api/admin/hero', sendForm) .then(response => { console.log(response.data); // form 초기화 setName(''); setEmail(''); setSex({ male: true, female: false }); setAddress(''); setPowers({ flying: false, penetration: false, hacking: false, strength: false }); setPhoto(''); isFormInvalid = false; // toast addToast('등록되었습니다.', { appearance: 'success', autoDismiss: true }); }); } |
삭제 성공시 아래와 같은 토스트가 나타난다.