동아리에서 부원들끼리 소그룹을 만들어서 개발과 관련된 소규모 스터디를 진행하였습니다. 제가 선택한 그룹은 레거시 그룹이라는 우리가 이전에 했던 프로젝트에 어떤 코드를 썼는지를 리뷰하고 레거시로 남게 되거나 기능 개선이 필요한 경우에 대해서 정보를 공유하고 코드를 개선하는 그룹입니다.
그래서 그룹활동에 취지에 맞게 이전에 코인 리코드 프로젝트에서 제가 신경써서 작업했던 state를 필요한 곳에서만 적재적소하게 사용하기 위해 했던 노력을 공개하고 실제 어떤 개선이었는지에 대해서 소개하겠습니다.
state와 관련되어서 어떤 것이 문제인지 간단한 예제를 제시해보겠습니다.
(비장한 소그룹)
우리는 코인 리코드 작업 중에 어떤 문제가 있는지 탐색하던 중 로그인 화면에서 문제를 찾을 수 있었습니다.
해당 아래 이미지를 확인해주세요
위의 코드에서 state
를 통해 ID와 패스워드의 value
를 추적하게 되고 입력 하나마다 이를 검사하고 리렌더링이 일어나는 것을 chrome의 react-devTool-extension
을 통해 알 수 있습니다.
이런 현상이 일어나는지에 대한 내용을 한번 예시를 통해 소개해드리겠습니다.
import React, { useState } from 'react';
export default function MyApp() {
const [name, setName] = useState('');
const [address, setAddress] = useState('');
return (
<>
<label>
Name{': '}
<input value={name} onChange={(e) => setName(e.target.value)} />
</label>
<br />
<label>
Address{': '}
<input value={address} onChange={(e) => setAddress(e.target.value)} />
</label>
<Greeting name={name} />
</>
);
}
const Greeting = function Greeting({ name }) {
console.log('Greeting was rendered at', new Date().toLocaleTimeString());
return (
<h3>
Hello{name && ', '}
{name}!
</h3>
);
};
해당 입력 폼에서 입력할 때 마다 console.log
가 발생하면서 리렌더링이 발생하는 상황을 알 수 있습니다.
해당 폼에 적용된 로직은 위에서 보게된 이미지와 동일하게 작성되었습니다.
state
는 리액트 렌더링을 발생시키기 됩니다. 공식문서에 따르면
useState
훅은 두 가지를 제공합니다:렌더링 사이에 데이터를 유지하는 상태 변수.
변수를 업데이트하고 React가 컴포넌트를 다시 렌더링하도록 트리거하는 상태 설정 함수.reference: https://react.dev/learn/state-a-components-memory
useState 훅을 사용할 때 setState 함수를 호출하면 해당 컴포넌트는 상태가 변경되었다고 판단하여 리렌더링이 발생합니다. 이는 React의 기본 동작 방식 중 하나입니다. (물론 리액트에서 렌더링 일으키는 건 더 있습니다)
그렇다면 이것을 극복할 방법은 없는 것일까요?
저희는 이 해결 방안에 대해 총 2가지를 논의하였습니다.
그렇다면 위의 예제 코드를 2가지 방식에 맞게 개선해보았습니다.
1번 방법인 memo를 통해 리렌더링을 방지할 수 있었습니다.
import React, { memo, useState } from 'react';
export default function MyApp() {
const [name, setName] = useState('');
const [address, setAddress] = useState('');
return (
<>
<label>
이름 {': '}
<input value={name} onChange={(e) => setName(e.target.value)} />
</label>
<br />
<label>
Address{': '}
<input value={address} onChange={(e) => setAddress(e.target.value)} />
</label>
<Greeting name={name} />
</>
);
}
const Greeting = memo(function Greeting({ name }) {
console.log('Greeting was rendered at', new Date().toLocaleTimeString());
return (
<h3>
Hello{name && ', '}
{name}!
</h3>
);
});
React.memo
는 React의 렌더링 최적화를 위해 설계된 고차 컴포넌트(Higher-Order Component, HOC)
입니다
React.memo
는 컴포넌트에 전달되는 props
가 변경되었는지 확인하기 위해 얕은 비교를 수행합니다. 얕은 비교는 객체의 최상위 레벨의 값들만을 비교하는 방법입니다. 즉, props
객체나 그 내부의 객체가 가리키는 값이나 참조가 이전 렌더링과 비교해 변했는지 확인합니다.
예제 코드에서는 React.memo
를 사용함으로써, name
값에 변화가 없다면 <Greeting />
컴포넌트의 재렌더링을 건너뛰게 됩니다.
2번 방법인 useRef와 useImpretive를 활용한 리렌더링 방지입니다.
import React, {
useState,
useRef,
useImperativeHandle,
forwardRef,
} from 'react';
export default function MyApp() {
const [name, setName] = useState('');
const passwordRef = useRef();
return (
<>
<label>
Name{': '}
<input value={name} onChange={(e) => setName(e.target.value)} />
</label>
<br />
<PasswordInput ref={passwordRef} />
<Greeting name={name} />
</>
);
}
const PasswordInput = forwardRef((props, ref) => {
// input 요소에 대한 ref 생성
const inputRef = useRef();
useImperativeHandle(ref, () => ({
// 외부에서 접근할 수 있도록 getPassword 함수 제공
getPassword: () => inputRef.current.value,
}));
return (
<label>
Password{': '}
<input
type="password"
ref={inputRef} // input 요소에 ref 연결
defaultValue="" // controlled 대신 uncontrolled 컴포넌트로 사용
/>
</label>
);
});
const Greeting = ({ name }) => {
console.log('Greeting was rendered at', new Date().toLocaleTimeString());
return (
<h3>
Hello{name && ', '}
{name}!
</h3>
);
};
useImpretiveHandle과 forewardRef를 활용하게 되는데, useImpretiveHandle은 리액트 훅 중 하나로, 부모 컴포넌트에게 노출할 ref 핸들을 사용자가 직접 정의할 수 있게 (ref로 노출 시키는 노드의 일부 메서드만 노출할 수 있게) 해주는 훅입니다.
forwardRef는 사용하면 구성 요소가 참조를 사용하여 DOM 노드를 상위 구성 요소에 노출할 수 있습니다.
useRef
훅은 React 컴포넌트에서 참조(ref)
를 생성하고 접근할 수 있게 해줍니다. 이 훅이 반환하는 객체(ref
객체)는 .current
프로퍼티를 통해 참조된 DOM 요소나 React 엘리먼트에 직접 접근할 수 있게 해줍니다. 여기서는 passwordRef
를 생성하여 PasswordInput
컴포넌트에 전달하고 있습니다. 이를 통해 부모 컴포넌트(MyApp)
가 자식 컴포넌트(PasswordInput)
내부의 함수에 접근할 수 있습니다.
useState
의 경우에는 상태가 변경되면 컴포넌트가 리렌더링되지만, .current
프로퍼티를 활용함을 통해 직접 접근하여 변경을 일으키기 때문에 리렌더링을 발생 시키지 않습니다.
1번과 2번 방법 둘 다 최적화라는 방식에서 결과가 개선된 것을 확인했습니다. 두 기능의 결과에 대한 차별점을 확인해보겠습니다.
React.memo
를 사용함으로써, name
값에 변화가 없다면 <Greeting />
컴포넌트의 재렌더링을 건너뛰게 됩니다. 이의 경우에는 Address가 변경되어도 name의 상태가 유지됩니다.ref
객체)는 .current
프로퍼티를 통해 참조된 DOM 요소나 React 엘리먼트에 직접 접근할 수 있게 해줍니다. 여기서는 passwordRef
를 생성하여 PasswordInput
컴포넌트에 전달하면서 최적화 합니다.그 중, React 공식문서에서 React.memo의 사용법과 관련해서 DeepDive 내용에서 이러한 내용이 있었습니다
If your app is like this site, and most interactions are coarse (like replacing a page or an entire section), memoization is usually unnecessary. On the other hand, if your app is more like a drawing editor, and most interactions are granular (like moving shapes), then you might find memoization very helpful.
"만약 당신의 앱이 이 사이트처럼 대부분의 상호작용이 페이지를 교체하거나 전체 섹션을 대체하는 것과 같은 대략적인(interaction) 형태라면, 메모이제이션은 보통 불필요합니다. 반면에, 당신의 앱이 드로잉 에디터와 같고 대부분의 상호작용이 도형을 이동하는 것과 같이 세밀한(granular) 형태라면, 메모이제이션이 매우 유용할 수 있습니다."
다음 글에서는 memo와 관련된 내용에 대해 다이브 해보는 경험을 작성해보겠습니다.