리액트 사용중에 페이지가 렌더링되기 이전에 서버에서 얻은 데이터를 활용해서 페이지를 보여줘야 하는 경우가 생겼습니다. 처음부터 제대로 리액트를 공부하지 않고 실제로 만들어보면서 모르는 것이 있을때 마다 구글에 물어봐서 사용하였습니다. 빠른 시간안에 결과물이 나와야 하는 상황이라 거의 무지성으로 리액트를 시작하게 되었는데요. 프로젝트가 마무리되면 다시한번 제대로 공부해 봐야겠다고 생각했습니다.
우선 제가 구현하고 싶었던 최종 결과는 다음과 같습니다. 페이지를 새로고침 할 때 마다 서버에 api를 요청하여 화면에 뿌려주고 이메일, 매출액, 영업이익, 당기순이익, 기준일자는 onChange를 적용하여 수정이 가능해야 했습니다.

useState와 useEffect 활용하기
처음에는 api response를 단순히 useState에 넣어주고 useEffect로 새로고침시에 랜던링 해주면 될것 같다고 생각했습니다. (아래는 정상적으로 돌아가지 않는 코드입니다.)
const SaleModify: React.FC<SaleModifyProps> =({location}) =>{
const query = queryString.parse(location.search);
const accessToken = query["id"];
const [saleInfo,setSaleInfo] = useState<any>();
useEffect(()=>{
handleGetSale({accessToken});
},[]);
const { mutateAsync: handleGetSale } = useMutation(getSaleWithAccessTokenApi, {
onSuccess: ({ success, error, sale}) => {
if (success) {
console.log('getSale Success!');
console.log(sale);
setSaleInfo(sale);
} else {
console.log('getSale failed: ', error);
}
},
});
return(
<>
<LabelWrapper>
<InputWithLabel
label='회사명'
value={saleInfo.companyName}
readOnly={true}>
</InputWithLabel>
</LabelWrapper>
<LabelWrapper>
<InputWithLabel
label='사업자번호'
value={saleInfo.businessNum}
readOnly={true}>
</InputWithLabel>
</LabelWrapper>
<LabelWrapper>
<InputWithLabel
label='이메일'
value={saleInfo.email}>
</InputWithLabel>
</LabelWrapper>
<LabelWrapper>
<InputWithLabel
label='매출액'
value={saleInfo.totalSales}>
</InputWithLabel>
</LabelWrapper>
<LabelWrapper>
<InputWithLabel
label='영업이익'
value={saleInfo.netIncome}>
</InputWithLabel>
</LabelWrapper>
<LabelWrapper>
<InputWithLabel
label='당기순이익'
value={saleInfo.operatingProfit}>
</InputWithLabel>
</LabelWrapper>
<LabelWrapper>
<InputWithLabel
label='기준일자(년월)'
value={saleInfo.date}>
</InputWithLabel>
</LabelWrapper>
</>
)
}
그 결과는.... 하..... 무지성 코딩의 결과 처참하다.....

자세히 읽어보면 undefined된 props를 읽으려다 실패한거 같습니다.
처음에는 useEffect를 사용하면 새로고침 후에 제일 먼저 렌더링되는줄 알았습니다. 그래서 아래와 같이 useEffect에서 api call을 하고 useMutation에서 useState를 해주니 당연히 페이지 렌더링 이전에 모든 데이터가 준비 되었있을 것이라 생각했습니다.
const [saleInfo,setSaleInfo] = useState<any>();
useEffect(()=>{
handleGetSale({accessToken});
},[]);
const { mutateAsync: handleGetSale } = useMutation(getSaleWithAccessTokenApi, {
onSuccess: ({ success, error, sale}) => {
if (success) {
console.log('getSale Success!');
console.log(sale);
setSaleInfo(sale);
} else {
console.log('getSale failed: ', error);
}
},
});
이는 React LifeCycle에 대해 알지 못한상태에서 뛰어들어 발생한 이유라고 생각했습니다.
React LifeCycle

리액트 LifeCycle은 크게 mount, update, unmount있으며 여기서 마운트는 DOM이 생성되고 웹 브라우저 상에서 나타나는 것을 뜻하고, 반대로 언마운트는 DOM에서 제거되는 것을 뜻합니다.
주의하여 볼 것은 업데이트 부분인데, 업데이트는 다음과 같은 4가지 상황에서 발생합니다.
- props가 바뀔 때
- state가 바뀔 때
- 부모 컴포넌트가 리렌더링 될 때
- this.forceUpdate로 강제로 렌더링을 트리거할 때
리액트 LifeCycle을 정말 잘 설명해 주신 분이 계셔서 많이 참고 했습니다.
리액트 라이프사이클의 이해
시작하기 전에 리액트 라이프 사이클을 원래 알고는 있었지만 정확하게 한번도 정리해본 적이 없는 것 같아서 글을 쓰게 되었다. 더불어 리액트 라이프 사이클과 최근 사용되는 Hooks와도 비교해
kyun2da.dev
React LifeCycle을 공부하다 보니 완전히 다른 개념으로 생각하고 있었습니다. class component를 사용하였다면 componentDidMount()를 이용하여 DOM이 생성되기 이전에 api호출 과 같은 사전 데이터를 처리해 줄수 있습니다. 저는 Hooks를 사용하였기 때문에 useEffect를 이용하여 componentDidMount를 호출 할 수 있었습니다.
하지만 여기서 간과하고 넘어갔던 사실은 componentDidMount 이전에 render가 실행이 된다는것 이였습니다.
// Class
class Example extends React.Component {
render() {
return <div>컴포넌트</div>
}
}
// Hooks
const example = () => {
return <div>컴포넌트</div>
}
에러가 났던 이유는 당연히 render를 하게되면 props값이 결정되야 하는데, 제가 작성한 코드를 보면 useEffect보다 render가 먼저 처리되어 saleInfo에서 undefined가 발생하는 것이 맞았습니다.
이를 해결하기 위해 생각해낸 방법은 saleInfo를 초기화 하고 useEffect를 이용하여 api를 호출 한다음 useState로 다시 렌더링을 하는 방법이였습니다.
const SaleModify: React.FC<SaleModifyProps> =({location}) =>{
const query = queryString.parse(location.search);
const accessToken = query["id"];
const [saleInfo,setSaleInfo] = useState<any>({companyName:"",businessNum:"",email:"",totalSales:0,netIncome:0,operatingProfit:0,date:null});
useEffect(()=>{
handleGetSale({accessToken});
},[]);
const { mutateAsync: handleGetSale } = useMutation(getSaleWithAccessTokenApi, {
onSuccess: ({ success, error, sale}) => {
if (success) {
console.log('getSale Success!');
console.log(sale);
setSaleInfo(sale);
} else {
console.log('getSale failed: ', error);
}
},
});
return(
<>
<LabelWrapper>
<InputWithLabel
label='회사명'
value={saleInfo.companyName}
readOnly={true}>
</InputWithLabel>
</LabelWrapper>
<LabelWrapper>
<InputWithLabel
label='사업자번호'
value={saleInfo.businessNum}
readOnly={true}>
</InputWithLabel>
</LabelWrapper>
<LabelWrapper>
<InputWithLabel
label='이메일'
value={saleInfo.email}>
</InputWithLabel>
</LabelWrapper>
<LabelWrapper>
<InputWithLabel
label='매출액'
value={saleInfo.totalSales}>
</InputWithLabel>
</LabelWrapper>
<LabelWrapper>
<InputWithLabel
label='영업이익'
value={saleInfo.netIncome}>
</InputWithLabel>
</LabelWrapper>
<LabelWrapper>
<InputWithLabel
label='당기순이익'
value={saleInfo.operatingProfit}>
</InputWithLabel>
</LabelWrapper>
<LabelWrapper>
<InputWithLabel
label='기준일자(년월)'
value={saleInfo.date}>
</InputWithLabel>
</LabelWrapper>
</>
)
}
결과는 .... 반정도 성공!!!

하지만 문제는 위의 사진에서 이메일, 매출액, 영업이익, 당기순이익, 기준일자(년월)은 수정 가능하게 하고 싶었습니다. 현재는 Input value에 값을 넣어버려서 수정이 불가능한 상태입니다. value를 수정하기 위해서 onChange 이벤트와 useState를 이용하여 다시 렌더링을 하면 실시간으로 반영될 수 있을것 이라 생각했습니다.
최종 코드
우선 useState, useCallBack을 이용하여 useMultipleInputs라는 Hook을 생성하였고 최종 코드는 아래와 같습니다.
import { useState, useCallback, ChangeEvent } from 'react';
type onChangeType = (e: ChangeEvent<HTMLInputElement>) => void;
type onResetType = () => void;
type useInputType = (
initialState: Record<any, any>,
) => [Record<any, any>, onChangeType, onResetType];
export const useMultipleInputs: useInputType = (
initialForm: Record<any, any>,
) => {
const [form, setForm] = useState(initialForm);
const onChange = useCallback((e) => {
const { name, value } = e.target;
setForm((form) => ({ ...form, [name]: value }));
}, []);
const reset = useCallback(() => setForm(initialForm), [initialForm]);
return [form, onChange, reset];
};
const SaleModify: React.FC<SaleModifyProps> =({location}) =>{
const query = queryString.parse(location.search);
const accessToken = query["id"];
const [saleInfo,setSaleInfo] = useState<any>({companyName:"",businessNum:"",email:"",totalSales:0,netIncome:0,operatingProfit:0,date:null});
useEffect(()=>{
handleGetSale({accessToken});
},[]);
useEffect(()=>{
resetInputs();
},[saleInfo]);
const { mutateAsync: handleGetSale } = useMutation(getSaleWithAccessTokenApi, {
onSuccess: ({ success, error, saleInfo}) => {
if (success) {
console.log('getSale Success!');
console.log(saleInfo);
setSaleInfo(saleInfo);
} else {
console.log('getSale failed: ', error);
}
},
});
const [sale, onChangeInputs, resetInputs] =
useMultipleInputs({
companyName: saleInfo["companyName"],
businessNum: saleInfo["businessNum"],
email: saleInfo["email"],
totalSales: saleInfo["totalSales"],
netIncome: saleInfo["netIncome"],
operatingProfit: saleInfo["operatingProfit"],
date: saleInfo["date"]
});
return(
<>
<LabelWrapper>
<InputWithLabel
label='회사명'
value={sale.companyName}
readOnly={true}>
</InputWithLabel>
</LabelWrapper>
<LabelWrapper>
<InputWithLabel
label='사업자번호'
value={sale.businessNum}
readOnly={true}>
</InputWithLabel>
</LabelWrapper>
<LabelWrapper>
<InputWithLabel
label='이메일'
value={sale.email}
onChange={onChangeInputs}
name={"email"}>
</InputWithLabel>
</LabelWrapper>
<LabelWrapper>
<InputWithLabel
label='매출액'
value={sale.totalSales}
onChange={onChangeInputs}
name={"totalSales"}>
</InputWithLabel>
</LabelWrapper>
<LabelWrapper>
<InputWithLabel
label='영업이익'
value={sale.netIncome}
onChange={onChangeInputs}
name={"netIncome"}>
</InputWithLabel>
</LabelWrapper>
<LabelWrapper>
<InputWithLabel
label='당기순이익'
value={sale.operatingProfit}
onChange={onChangeInputs}
name={"operatingProfit"}>
</InputWithLabel>
</LabelWrapper>
<LabelWrapper>
<InputWithLabel
label='기준일자(년월)'
value={sale.date}
onChange={onChangeInputs}
name={"date"}>
</InputWithLabel>
</LabelWrapper>
</>
)
}
마치며
처음에는 간단할 줄만 알았는데 막상 구현하다보니 예상치 못한 이슈가 발생하였고 2일 동안 삽질한 끝에 결국 해결할 수 있었습니다. 리액트를 제대로 공부하지 않은 상태에서 만들다 보니 여러가지 이슈들이 발생하였고, 아직 React LifeCycle에 대해 완벽히 이해한것은 아니지만 조금더 공부하여 다음번에는 Class를 이용해 mount, update, unmount등을 사용해 보도록 하겠습니다.
'React' 카테고리의 다른 글
React에서 DevExpress를 이용해 Grid Table 생성하기 (0) | 2022.02.15 |
---|