0. 버그 현황
버그 2개 발생
1) console.log(loading) 반복 렌더링 이슈
2) if 문 조건절안에 status 상태값 변경되었으나, navigate 적용 안되는 이슈
- loginSlice.ts 에서 status 값(비동기 통신 여부에 따라 'loading','succeeded','fail' 을 받아와서 LoginForm.tsx 에서 status 값을 불러옴.
- 로그인 버튼을 누르면 handleSubmit 함수가 발동돼서 통신이 성공할 경우 loading 에서 succeeded로 상태가 변하는데(콘솔창 캡처 참고)
- 상태가 succeeded로 변하면 페이지로 이동할 줄알았는데 이동이 되지않음.
console.log(stats)를 해보면 succeeded는 그대로 출력됨
즉, 콘솔창에서 확인해봤을때 loading은 succeeded로 상태변화가 되었는데 왜 navigate 가 실행이 안되는것이냐!!
1. 버그 코드 : 리액트와 클로저의 관계
// LoginForm.tsx
function LoginForm() {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const [toggleType, setToggleType] = useState("BUYER");
const loginError = useAppSelector((state: RootState) => state.login.error);
// console.log(loginError);
const status = useAppSelector((state: RootState) => state.login.status);
console.log(status);
const [inputs, setInputs] = useState({
username: "",
password: "",
});
// 아이디 & 비밀번호 입력
const handleData = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setInputs({ ...inputs, [name]: value });
};
// 로그인 폼 제출
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const loginData = {
username: inputs.username,
password: inputs.password,
login_type: toggleType,
};
await dispatch(fetchLogin(loginData));
if (status === "succeeded") {
navigate("/");
}
};
return (
<section>
<h2 className="hidden">로그인 페이지</h2>
<ToggleBtn toggleType={toggleType} setToggleType={setToggleType} />
<S.LoginForm onSubmit={handleSubmit}>
<S.LoginInput
placeholder="아이디"
type="text"
onChange={handleData}
name="username"
value={inputs.username}
// required
/>
<S.LoginInput
placeholder="비밀번호"
type="password"
onChange={handleData}
name="password"
value={inputs.password}
// required
/>
{loginError ? <S.ErrorText>{loginError}</S.ErrorText> : null}
<S.LoginBtn type="submit" size="md">
로그인
</S.LoginBtn>
</S.LoginForm>
</section>
);
}
export default LoginForm;
1) loading 반복 렌더링 이슈
✅ 리액트 렌더링
console.log(status) 를 하면 input 값이 한글자만 변경해도 'loading' 상태가 계속 업데이트 되는데. 왜 계속 렌더링 되는걸까?
🔸 리액트 기본 개념
- 컴포넌트 내에서 상태가 변경될 때마다 리액트는 해당 컴포넌트를 다시 렌더링 한다.
- 왜냐하면, 리액트가 변경된 상태를 화면에 반영하기 위해 컴포넌트를 다시 렌더링 하는것. 이 과정에서 리액트는 가상DOM 을 사용하여 변경된 부분만 실제 DOM 에 업데이트 함으로써 성능을 최적화 한다.
- 예를 들어, useState나 useReducer와 같은 상태 관리 훅을 사용하여 컴포넌트의 상태를 관리할 때, 상태가 변경되면 컴포넌트가 다시 렌더링 된다. 마찬가지로, useSelector와 같은 상태 관리 라이브러리를 사용하여 전역 상태를 구독하고 있는 컴포넌트도 상태가 변경될 때마다 다시 렌더링됩니다.
- 이 예시에서는 status 값이 'loading' 상태로 계속 동일하지만, input 값이 계속 상태 변경(입력값 변경)이 되고 있기에 다시 렌더링 되고 있는 것이다.
결국, console.log(status)가 'loading' 상태로 계속 출력되는 것은 useAppSelector 훅과 컴포넌트의 다시 렌더링될 때마다 실행되는 것 때문이다.
🔸 추가 useAppSelector 개념
const status = useAppSelector((state: RootState) => state.login.status);
useAppSelector 훅은 react-redux에서 제공하는 useSelector를 기반으로 작성되어 있으며, 이 훅은 Redux store의 일부 상태를 구독한다.
상태 구독 : 구독한 상태가 변경될 때마다 관련된 컴포넌트를 다시 렌더링하는 것
여기서는 status를 구독하고 있기 때문에, status가 변경되면 해당 컴포넌트가 다시 렌더링된다.
하지만, status 는 계속 'loading' 값으로 구독한 상태가 변경되지 않았다.
대신, 'LoginForm' 컴포넌트의 inputs 상태가 변경되고 있기에, inputs 상태가 변경될때마다 컴포넌트가 다시 렌더링 된다.
입력이 변경될 때마다 컴포넌트가 다시 렌더링되기 때문에, 이 때마다 console.log(status)가 실행되는 것. 이는 status 값이 변경되었을 때뿐만 아니라, 컴포넌트의 다른 상태 값(예: inputs.username 또는 inputs.password)이 변경되어 컴포넌트가 다시 렌더링될 때마다 발생함.
나는 status 를 디버깅 목적으로 콘솔창에 출력하고 있기때문에, 출력이 불필요하게 반복되는것을 최소화 하기위해
'useEffect'를 사용해서 'status' 상태에 대한 디버깅을 할 수 있다.
useEffect(() => { console.log(status); }, [status]); // status 상태가 변경될 때만 console.log(status) 실행
2) if 문 조건절안에 status 상태값 변경되었으나, navigate 적용 안되는 이슈
(부제: 클로저 트랩)
if (status === "succeeded") {
navigate("/");
}
분명 로그인 버튼 클릭 후 => console.log(status)를 보면 loading 에서 succeeded로 바껴서 출력했지만,
status 변수 값이 제대로 갱신이 되지 않아서 navigate 가 실행이 안되고 있다.
status 변수가 왜 업데이트가 되지 않을까?
✅리액트와 클로저의 관계
- 리액트에서는 함수 컴포넌트의 바디가 한 번 실행될 때마다 이를 클로저로 간주한다.
🔸클로저
함수가 생성될 당시의 외부 변수를 기억하여 생성 이후에도 계속 접근 가능한 것이 클로저
이후에 외부 변수가 변경되더라도, 클로저로 인해 해당 함수는 그 변수의 이전 값을 계속 참조하게 된다.
예를 들어 myFunc은 outer에서 inner 함수를 반환받습니다. myFunc이 실행되면 inner가 생성될 때 name 값을 기억하여 song을 alert 합니다.
🔸클로저 트랩
상태 업데이트나, 이벤트 처리와 관련된 문제
상태 업데이트 함수에서 클로저로서 유지되는 이전 상태값을 사용하려고 할 때 발생할 수 있음.
이전 상태 값은 상태 업데이트 함수가 선언된 시점에서의 값이기 때문에, 상태가 업데이트 되어도 변경되지 않는다. 따라서 예상한 대로 동작하지 않을 수 있다.
handleSubmit 함수가 선언될 때 handleSubmit 외부의 status 변수를 가져와서 사용하지만, 상태가 변경되면 해당 함수 즉, handleSubmit 내부의 status의 변수는 업데이트 되지 않는다.
🔶클로저와 이벤트 핸들러 정리 🔶
1. 함수형 컴포넌트는 상태와 이벤트 핸들러 등의 로직을 포함할 수 있다.
2. 이벤트 핸들러와 같은 함수들은 외부 변수를 참조하고 사용할 수 있다.
3. 이벤트 핸들러가 외부 변수를 참조하는 경우, 클로저가 발생하며, 해당 변수들을 기억하게 된다.
4. 이후 외부 변수가 변경되더라도, 클로저로 인해 이벤트 핸들러는 해당 변수의 이전 값을 기억 하게 된다.
클로저를 사용하면, 리액트 컴포넌트에서 상태와 로직을 쉽게 관리할 수 있다.
그러나 클로저로 인해 발생할 수 있는 문제도 있다. 예를 들어, 함수형 컴포넌트가 여러 번 렌더링되면, 각 렌더링에 해당하는 클로저가 생성되어 이벤트 핸들러가 참조하는 변수 값이 항상 최신 값이 아닐 수 있다.
2. 버그 해결 : useEffect (보충하기)
useEffect(() => {
if (status === "succeeded") {
navigate("/");
}
},[status]);
useEffect를 사용하면, 컴포넌트가 렌더링 될 때마다 특정 작업을 실행할 수 있으며, 이를 통해 최신 상태를 반영할 수 있다.
useEffect 는 두번째 매개변수인 의존성 배열이 있다.
의존성 배열이 undefined와 null 이면 리렌더링 마다 실행되고, 매개변수가 [] 빈 배열이라면 한번만 실행된다.
매개변수가 [state] 라면 state가 바뀔 때만 다시 실행된다.
클로저 트랩은 의존성 배열만 정확하게 설정해주면 된다. 이렇게 하면 상태가 바뀔때마다 콜백함수가 다시 실행되고, 새로운 상태를 참조하게 된다.
출처 : https://velog.io/@superlipbalm/the-closure-trap-of-react-hooks
https://simsimjae.tistory.com/m/400
'프로젝트 이모저모 > HoduMarket 프로젝트' 카테고리의 다른 글
오픈마켓) 라이트하우스 성능점수 13점 -> 67점으로 3배 껑충 (0) | 2023.04.17 |
---|---|
오픈마켓) 자동,페이지네이션 캐러셀 슬라이드 만들기 (feat. 리액트/타입스크립트) (0) | 2023.04.17 |
오픈마켓) 회원가입 에러메세지 버그 (0) | 2023.04.07 |
오픈마켓) 회원가입 에러메세지 - react-hook-form 버그 (0) | 2023.04.07 |
리액트+타입스크립트) styled-components theme 설정하기 (0) | 2023.03.14 |