개인 프로젝트를 진행하다가 이상한 렌더링을 발견했다. input 창에 글자를 입력할 때마다 렌더링이 발생한다.
파란색 프레임이 생성되는 것은 React Develop Tool의 렌더링 표시 기능인데, 렌더링이 될 때마다 표시된다.
보다시피 글자 하나 입력 시 렌더링 된다. 그리고 이메일 input창에 값을 입력했는데 전체 페이지가 렌더링 되는 것을 볼 수 있다. 그냥 봐도 거꾸로 봐도 백덤블링하면서 봐도 비효율적인 것을 알 수 있다.
원래 이런 건가 싶어 리액트로 만든 다른 사이트를 엿보았다.
오늘의집과 직방 또한 이러한 현상이 나타났다.
input에 글자를 입력하는데 왜 렌더링이 일어난다는 것에 대한 의문.
잦은 렌더링을 피해야 하는 것은 상식이기 때문에 해결하고 싶음 마음.
두 포인트를 해결하기 위해 이 문제에 대해 공부해 봤다.
그러다 이 두 가지 문제를 속 시원하게 해결해 준 블로그를 찾았다!
https://jaehoon0822-blog.vercel.app/blog/2023-04-15-react-hooks-from
react-hook-from 에 대한 고찰
react-hook-from 을 왜 사용하며, 사용하면서 알아야할 개념들
jaehoon0822-blog.vercel.app
이 블로그를 꼭꼭, 두번, 세번 보길 바란다.
State Colocation(상태공존) 개념과 더불어 uncontrolled component, controlled component에 대해서도 정리해뒀다.
리액트에서의 렌더링 조건
해당 블로그에서 말하길 렌더링 되는 경우는 4가지 상황으로 보면 된다고 한다.
1. State가 변경될 때
2. 부모 컴포넌트가 랜더링 될 때
3. 새로운 Props 값을 받을 때
4. Props가 변경될 때
심플하다. 이 4가지만 조건만 잘 알고 있는다면 불필요한 렌더링을 줄이고 수월하게 최적화 작업을 할 수 있다.
내 프로젝트의 경우 1번과 2번에 해당 된다.
input창에 값이 입력될 경우 onChange 함수를 통해 email과 password의 State 값이 변경되면서 LoginIndex라는 컴포넌트 전체에서 렌더링이 일어나는 것. useState가 LoginIndex라는 컴포넌트에 선언되어 있기 때문이다.
export default function LoginIndex(): JSX.Element {
const router = useRouter();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [, setAccessToken] = useRecoilState(accessTokenState);
const [loginUser] = useMutationLoginUser();
const onChangeEmail = (e: ChangeEvent<HTMLInputElement>): void => {
setEmail(e.currentTarget.value);
};
const onChangePassword = (e: ChangeEvent<HTMLInputElement>): void => {
setPassword(e.currentTarget.value);
};
State Colocation
이를 피하기 위해서는 State Colocation 개념을 이용하는 것인데. 간단히 설명하자면 useState 선언을 좀 더 긴밀하게? 하는 거다.
블로그의 예시를 참고해서 다음과 같은 테스트를 만들어 봤다.
import { useState } from "react";
const slow = (time) => {
const done = Date.now() + time;
while (done > Date.now()) {
//...오래걸리는 작업
}
};
//작업이 오래 걸리는 컴포넌트
const SlowComponent = ({ time, onChange }) => {
slow(time);
return (
<div>
작업 시간은?
<input
type="number"
value={time}
onChange={(e) => onChange(Number(e.target.value))}
style={{ backgroundColor: "white" }}
/>
</div>
);
};
//일반적으로 처리되는 컴포넌트
const NormalComponent = ({ time, value, onChange }) => {
return (
<div>
무언가 입력해보세요
<input
value={value}
onChange={(e) => onChange(e.target.value)}
style={{ backgroundColor: "white" }}
/>
<div>
{value ? `${value}'은 ${time}ms 뒤에 실행됩니다` : "값을 입력하세요"}
</div>
</div>
);
};
// 두 컴포넌트를 한 컴포넌트 안에서 import
const AppComponent = () => {
const [value, setValue] = useState("");
const [time, setTime] = useState(300);
return (
<>
<NormalComponent time={time} value={value} onChange={setValue} />
<br></br>
<SlowComponent time={time} onChange={setTime} />
</>
);
};
export default function TestPage() {
return <AppComponent></AppComponent>;
}
어후 코드가 긴데, 요약하자면 다음과 같다.
1. SlowCompnent에는 slow라는 함수를 사용해서 오래 걸리는 작업으로 만든다.
2. NormalComponent는 작업 시간이 오래걸리지 않는 일반적인 컴포넌트다.
3. 이 두 컴포넌트를 AppComponent라는 하나의 컴포넌트에 집어넣고 useState를 통해 value 값을 전달한다.
테스트 결과는 아래와 같다.
"무언가 입력해보세요"라는 input창은 분면 NormalComponent에 존재하는데, SlowComponent의 0.3초의 작업 시간에 영향을 받아서 느리게 입력된다. 이것은 부모 컴포넌트 격인 AppComponent의 value라는 useState 값이 변경되면서 함께 리렌더링 되기 때문이다.
사실 리액트 사용자라면 바로 이해할 원리인데 오래 기억하기 위해 굳이굳이 테스트 코드로 만들어보면서 직접 해봤다. 그럼 이제 이것을 해결해보겠다. 바로 State Colocation 개념을 이용해서!
useState를 좀 더 독립적으로, 좁은 스코프로, 작은 블럭 단위에서 사용하는 거다.
수정한 코드만 살펴보겠다.
const NormalComponent = ({ time }) => {
const [value, setValue] = useState("");
return (
<div>
무언가 입력해보세요
<input
value={value}
onChange={(e) => setValue(e.target.value)}
style={{ backgroundColor: "white" }}
/>
<div>
{value ? `${value}'은 ${time}ms 뒤에 실행됩니다` : "값을 입력하세요"}
</div>
</div>
);
};
const AppComponent = () => {
const [time, setTime] = useState(300);
// const [value, setValue] = useState(""); 해당 useState를 NormalComponent로 이동
return (
<>
<NormalComponent time={time} />
<br></br>
<SlowComponent time={time} onChange={setTime} />
</>
);
};
AppComponent에 존재하던 value State를 NormalComponent로 이동시켜서 부모 컴포넌트의 영향을 받지 않도록 했다.
엄청난 오타.. 더 이상 SlowComponent에 영향을 받지 않고 빠르게 처리되는 것을 확인할 수 있다.
길었는데 이 개념이 State Colocation이라고 이해했다. (제가 잘못 이해했다면 댓글로..)
Uncontrolled Component
아 그리고 이 현상을 막기 위한 방법이 한 가지가 더 있다. 바로 uncontrolled component로 사용하는 방법. useState로 값을 받는 것이 아니라 DOM 조작으로 컴포넌트 값을 변경하기 때문에 useState에 의한 렌더링이 일어나지 않는다. 이러한 컴포넌트를 uncontrolled component라고 한다.
react hook form
그렇다면 React에서 이런 uncontrolled component를 어떻게 조작할 수 있을까? ref를 사용해서 조작할수도 있지만 react hook form을 사용하면 더 간편하고 효과적으로 조작할 수 있다.
아래는 react hook form Docs에서 설명하는 예제인데, Controlled Form 방식과 React Hook Form 방식을 비교 테스트해볼 수 있다. react hook form만 해도 내용이 엄청 길어지기 때문에 이 부분은 여기서 생략하겠다.
결과적으로 나는 두번째 방법인 react hook form을 사용해서 불필요한 렌더링을 제거했다.
input 값을 입력해도 더 이상 렌더링 표시가 보이지 않는다.
form 태그를 추가하면서 flex 정렬이 제멋대로 이동했다. 요것은 차차 수정하도록..
'프로그래밍⚡️ > React' 카테고리의 다른 글
pending 된 api 요청 중도 취소하기 (0) | 2024.07.28 |
---|---|
리액트에서 일정 간격 반복적으로 api 호출하기 refetchInterval (1) | 2023.07.10 |
재사용성을 높이는 디자인과 컴포넌트에 대한 이야기 (1) | 2023.03.19 |
React-hook의 종류와 custom hook 사용기 (0) | 2023.02.24 |
함수 안에서 어떻게 setState가 두번 작동하지? (micro queue, macro queue) (0) | 2023.02.22 |