0. 로직 시작~! 쉬운건데 하나도 안 쉽다ㅜ
리액트 초보인 내가 처음 도전한 건 파티생성 페이지 이다. 파티 생성에서 입력한 것들을 하나의 json 객체로 만들어서 서버로 보내주면 된다. 별거 아닐 거라 생각했는데 생각보다 너무 고생했다.
1. 인풋을 가져오자
처음에는 인풋을 받아오기 위해 노마드코더가 알려준대로 하기로 했다. 순서는 다음과 같다.
- useState를 이용해 newparty, Setnewparty을 정의한다.
- input마다 name, value, onChange 를 설정하여 input에 입력이 들어올 때 마다 감지하여 newparty에 저장한다.
- 이제 화면의 input들과 나의 newParty가 일치 한다.
const [newParty, SetNewParty] = useState({
title: '',
content: '',
hashtags: [],
place: 'SINCHON',
goalNumber: 0,
});
const onChange = (event) => {
const { value, name } = event.target;
SetNewParty({
...newParty,
[name]: value,
});
};
2. 해시태그 맵핑 이슈
- 해시태그의 경우 입력받은 스트링을 리스트로 바꾸어서 서버로 보내주어야 했다. 그래서 사용할 유틸 함수 hashTagStringToList.js을 정의하고 완료버튼에 의해 onClick event가 일어나면 hastags 부분만 바꿔서 newParty를 서버에 보내려고 했다.
- 근데 해시태그 맵핍을 하고 서버로 보내야 하는데 계속 보내면서 맵핑을 하는게 아닌가?
- promise, async, await 등 까지 공부하면서 해보려고 했지만 계속 안됐다.....
3. 서버와 통신하자!
프론트와 백엔드의 대화 시작이다~
- REST API
- CRUD
등 중요한 개념들이 많다. 이외에도
- Swagger
- response, request
등등 다 자 잘고 있자~
암튼 파티생성에는 post에 해당하는 부분이고 fetch를 이용했다. 대충 아래와 같은 순서로 한다고 생각하면 된다
밑에는 각각의 과정을 따로 모듈화 한 것 이다.
1. request body를 올바르게 생성
2. fetch url을 보내고 받아온 response를 json으로 변환하여 사용
const requestOptions = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newParty),
};
const getPartyObj = (newparty) => {
if (window.confirm('파티를 생성하시겠습니까?')) {
fetch(`${SWAGGER_URL}/parties`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(newparty),
})
.then((response) => response.json())
.then((response) => {
console.log(response);
});
} else {
console.log('파티 만들기 취소');
}
};
4. 암튼 실패
실패의 요인은
- hashtaga의 맵핑과 post 순서가 잘못됌
- 백에서 잘못된 에러코드를 반환함(400과 함께 에러메세지를 받아야 하는데 500이옴, 이건 백이 알아서 잘 해결했다. 이제 잘 보내기만 하면 된다.)
5. 해결1. form과 submit 이벤트 활용
- form을 활용하면 submit 이벤트가 일어날 때 form 태그 내부의 모든 input값을 가져올 수 있다. 즉 useState로 input의 값을 실시간을 받아오지 않아도 되는것이다(그렇다면 왜 그렇게 노마드 코더및 많은 사람들은 가르쳐주는가.?)
- 하지만 나는 파티생성 페이지를 두개의 컴포넌트로 분리하여 만들어서 inputs와 submit 버튼을 발생시킬 button이 동시에 존재하지 않는다. 그래서 useRef를 활용하기로 했다.
6. 해결2. useRef
- useRef는 어디에서나 HTML element를 특정하여 가져오게 할 수 있는 매우매우 좋은 훅이다.
- 그래서 formRef를 만들고 두개의 컴포넌트에 props로 각각 전달을 해주고 결론적으로 2번 컴포넌트의 ref={formRef} form을 1번 컴포넌트에서 감지할수 있게 해준다..!
- 그래서 1번 컴포넌틑에서 클릭이벤트의 콜백으로 submit 이벤트를 발새하여 2번 컴포넌트에서 onsubmit 함수를 통해 submit 될 때 데이터를 처리하려고 했는데 왜인지 모르겠지만 event.preventDefault가 먹지를 않았다..!
7. 헤결3. useRef
- submit 이벤트가 아니라 click 이벤트에서 전부 해결하기로 결정
- formRef로 form Data를 가져오는 것은 잘 됌!
const onClick = () => {
const form = new FormData(formRef.current);
const newParty = {
title: form.get('title'),
content: form.get('content'),
hashtags: hashTagStringToList(form.get('hashtags')),
place: form.get('place'),
goalNumber: form.get('goalNumber'),
};
createPartyObj(newParty);
};
8. 추가 홈으로 라우팅
- useNavigate을 이용
- withRouter는 react-router-dom v6에서 사라졌었다..!
function NewParty() {
const formRef = useRef();
const navigate = useNavigate();
const createPartyObj = (newparty) => {
if (window.confirm('파티를 생성하시겠습니까?')) {
fetch(`${SERVER_URL}/parties`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(newparty),
})
.then((response) => response.json())
.then((response) => {
if (response.statusCode === 400) alert(response.errors[0].errorMessage);
else if (response.statusCode === 200) {
alert('파티 생성 성공');
navigate('/');
}
});
} else {
console.log('파티 만들기 취소');
}
};
React Router v6 업데이트 정리 (velog.io)
React Router v6 업데이트 정리
velopert님의 영상을 토대로 정리한 블로그 글입니다.동영상으로 보실분들은 velopert님의 유튜브 영상을 시청해주세요!React Router v6가 정식으로 릴리즈 되었다. 공식문서그 동안 사용했던 React Router
velog.io
9. 커스터 모달 만들기
- 파티 생성 성공 또는 실패 시 모달을 띄어야 하는데 window.confirm 은 넘 못생겨서 커스텀 모달을 만들어서 사용하기로 했다.
- 컨펌모달, 알람모달 두개를 만들어서 사용하기로 했다.
- 컨펌모달의 경우 확인 취소 버튼이 있어야 하고,
- 알람모달의 경우 닫기 버튼 또는 모달 밖을 클릭하여 모달을 닫을 수 있어야 한다.
function ConfirmModal({ visible, children, onConfirm, onDisconfirm }) {
return (
<>
<ModalOverlay visible={visible} />
<ModalWrapper tabIndex="-1" visible={visible}>
<ModalInner tabIndex="0">
{children}
<button onClick={onConfirm}>확인</button>
<button onClick={onDeny}>취소</button>
<button onClick={onDisconfirm}>취소</button>
</ModalInner>
</ModalWrapper>
</>
);
}
function AlertModal({ visible, children, closable, maskClosable, onClose }) {
const onMaskClick = (e) => {
if (e.target === e.currentTarget) {
onClose(e);
}
};
const close = (e) => {
if (onClose) {
onClose(e);
}
};
return (
<>
<ModalOverlay visible={visible} />
<ModalWrapper tabIndex="-1" visible={visible}>
<ModalInner tabIndex="0">{children}</ModalInner>
<ModalWrapper onClick={maskClosable ? onMaskClick : null} tabIndex="-1" visible={visible}>
<ModalInner tabIndex="0">
{closable && <icons.CancelIcon onClick={close} />}
{children}
</ModalInner>
</ModalWrapper>
</>
);
}
- 이후 피드백을 받아 두 개의 모달중 type props를 받아 한개를 취사선택해서 사용하도록 개선하였다.
function Modal({ type, visible, children, onClose, onConfirm, onDisconfirm }) {
const onMaskClick = (e) => {
if (e.target === e.currentTarget) {
onClose(e);
}
};
const close = (e) => {
if (onClose) {
onClose(e);
}
};
if (type == 'CONFIRM') {
return (
<>
<ModalOverlay visible={visible} />
<ModalWrapper tabIndex="-1" visible={visible}>
<ModalInner tabIndex="0">
{children}
<button onClick={onConfirm}>확인</button>
<button onClick={onDisconfirm}>취소</button>
</ModalInner>
</ModalWrapper>
</>
);
} else if (type == 'ALERT') {
return (
<>
<ModalOverlay visible={visible} />
<ModalWrapper onClick={onMaskClick} tabIndex="-1" visible={visible}>
<ModalInner tabIndex="0">
<icons.CancelIcon onClick={close} />
{children}
</ModalInner>
</ModalWrapper>
</>
);
}
}