0w0

보수하기 쉬운 React Hooks 코드 방침

전제

이 글은 보수하기 쉬운 React Hooks 코드 방침에 대한 글임

방침은 tips에 가까운 것부터 원칙까지 잡다하게 있음

보수성 낮은 코드를 반면교사로 삼았고, 개인적 경험 기반임(생각날 때마다 추가할 수 있습니다.)

유념 부탁드립니다.

해소하고 싶은 것

해소하는 수법

useEffect는 1페이지 1개

나쁜 예: 유저 이벤트 처리

const [foo, setFoo] = useState('foo');

useEffect(() => {
  setFoo('bar');
});

useEffect(() => {
  setFoo('baz');
});

간략화해서 알기 어려울 수 있지만, 실제에는 여러 useEffect가 공통으로 하는 deps가 있어, 처리가 동시 발화하는 경우가 발생했다. 그래서 useEffect 내에 비동기처리가 있고, 처리 타이밍에 의해 순서를 정했다 결국 어떤 useEffect가 최종적으로 state를 갱신하는가 알 수 없다 재현 곤란한 버그/에러 발생이 일어났다

좋은 예: 유저 이벤트 처리

const [foo, setFoo] = useState('foo');

const onClickBar = () => setFoo('bar');

const onClickBaz = () => setFoo('baz');

useEffect를 제거하고, 여러 처리가 동시에 발화되지 않도록 했다.

후술한 Web 애플리케이션 대부분의 경우에 useEffect가 필요없는 경우가 대부분이다. useEffect를 1페이지에 1개하는 것이 현실적이라 보인다

브라우저에서 발생하는 이벤트는 다음과 같이 나뉜다

이 중 useEffect가 필요하지 않은 것은 유저 조작 이벤트 뿐.

많은 애플리케이션에서 페이지 트랜지션 이벤트, 유저 조작 이벤트 이외에는 잘 사용않는다(경험측).

사용하려해도 글로벌에서 이용하는 경우(예시: 네트워크 접속상태 감시) 혹은 1개의 컴포넌트에 관한 경우(예시: 통지유무게시)였다.

이래서 실제로 useEffect를 필요하는 것은 페이지 트랜지션 이벤트가 대부분이다. 그러니 1페이지에 1개를 쓰자

useEffect에 deps 자동보완 제거 주석을 남긴다

나쁜 예: 로드 함수

const [isLoading, setIsLoading] = useState < boolean > false;
const [res, setRes] = useState();

const load = useCallback(async () => {
  try {
    setIsLoading(true);
    const res = await FooApi();
    setRes(res);
  } finally {
    setIsLoading(false);
  }
}, [isLoading]);

useEffect(() => {
  load();
}, [load]);

useEffect 사용방법으로 렌더링할 때 로드 함수 발화가 있다. 이 경우의 대부분은 deps가 필요없다. 그러나 deps 조건부 기능쓰는 경우 쉽게 무한 루프 발생. 이걸 피하고 싶다

좋은 예: 로드 함수

const [isLoading, setIsLoading] = useState < boolean > false;
const [res, setRes] = useState();

const load = useCallback(async () => {
  try {
    setIsLoading(true);
    const res = await FooApi();
    setRes(res);
  } finally {
    setIsLoading(false);
  }
}, [isLoading]);

useEffect(() => {
  load();
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

// eslint-disable-next-line react-hooks/exhaustive-deps를 deps에 적용함으로 deps의 자동 조건부, 무한 루프를 예방한다.

state는 원형으로 한다

나쁜 예: 객체 state 갱신

const [fooObj, setFooObj] = useState<{
	name: string,
	familyName: string,
	firstName: string,
}>({
	name: "",
	familyName: "",
	firstName: "",
});

const onUpdateFamilyName = useCallback((newFamilyName: string) => {
	setFooObj(obj => {
		...obj,
		name: newFamilyName + obj.firstName,
		familyName: newFamilyName,
	});
}, []);

const onUpdateFirstName = useCallback((newFirstName: string) => {
	setFooObj(obj => {
		...obj,
		name: obj.familyName + newFirstName,
		firstName: newFirstName,
	});
}, []);

간단한 예를 적다보니 예시에는 큰 데미지는 없다

그러나 실제 fooObj는 큰 객체인 경우가 많다.

API request, response 객체가 그대로 1개의 state에 몰아있는 경우가 많기 때문.

큰 객체를 state로 관리하면, 1개의 필드마다 setState를 늘리자.

버그가 발생한 state 조사는 갱신이 왕왕있는 부분을 조사한다.

이런 조사처를 필드 수만큼 늘린다

그리고 setState가 다른 컴포터는에 전달되다면 조사 수단을 더 늘린다

결과, 버그 발생 지점 특정이 쉬워진다.

좋은 예: 원형 state 갱신

const [name, setName] = useState<string>('');
const [familyName, setFamilyName] = useState('');
const [firstName, setFirstName] = useState('');

const onUpdateFamilyName = useCallback((newFamilyName: string) => {
  setName(newFamilyName + firstName);
  setFamilyName(newFamilyName);
}, []);

const onUpdateFirstName = useCallback((newFirstName: string) => {
  setName(familyName + newFirstName);
  setFirstName(newFirstName);
}, []);

버그가 발생한 state 조사는 그 setState를 따라가면 될 뿐이다.

큰 객체를 다루기보다 조사, 코드 리팩토링 수고를 절감할 수 있다.

props에 플러그가 있다면 컴포넌트를 나눈다

나쁜 예: 작성/갱신이 같은 컴포넌트에 있다

const DialogContents: React.FC<{isAdd: boolean}> = ({
	isAdd: boolean,
}) => {
	// ...
	const onSubmit = useCallback(async () => {
		if (isAdd) {
			await FooRegisterApi();
			return;
		}
		await FooUpdateApi();
	}, []);

	return (
		<>
			<form onSubmit={onSubmit}>
				<label>Name</label>
				<input value={name} onChange=>{onChangeName}/>
				<label>ID</label>
				<input disabled={!isAdd} value={id} onChange=>{onChangeId}/>
				{isAdd &&
					<label>Email</label>
					<input value={email} onChange=>{onChangeEmail}/>
					<label>PhoneNumber</label>
					<input value={phoneNumber} onChange=>{onChangePhoneNumber}/>
				}
			</form>
		</>
	)
}

컴포넌트에 여러 책임이 있는 경우, 컴포넌트 도처에 쓸데없이 분기가 발생한다.

자주 보는 패턴은 작성과 갱신이 동시에 컴포넌트에서 일어나는 패턴

작성 / 갱신을 플러그로 분기해서 알맞은 API, 렌더링 항목을 제어한다.

이러면 분기가 많은 코드가 되서 읽기에 수고가 많이 든다

좋은 예: 작성/갱신 별 컴포넌트

const RegisterDialogContents: React.FC = () => {
	//...
	const onSubmit = useCallback(async () => {
		await FooRegisterApi();
	}, []);

	return (
		<>
			<form onSubmit={onSubmit}>
				<label>Name</label>
				<input value={name} onChange=>{onChangeName}/>
				<label>ID</label>
				<input value={id} onChange=>{onChangeId}/>
				<label>Email</label>
				<input value={email} onChange=>{onChangeEmail}/>
				<label>PhoneNumber</label>
				<input value={phoneNumber} onChange=>{onChangePhoneNumber}/>
			</form>
		</>
	)
}

const UpdateDialogContents: React.FC = () => {
	//...
	const onSubmit = useCallback(async () => {
		await FooUpdateApi();
	}, []);

	return (
		<>
			<form onSubmit={onSubmit}>
				<label>Name</label>
				<input value={name} onChange=>{onChangeName}/>
				<label>ID</label>
				<input disabled={true} value={id} onChange=>{onChangeId}/>
			</form>
		</>
	)
}

작성과 갱신은 구별해서 컴포넌트로 만들자.

도처의 분기를 제거해서 코드 읽기가 편해졌다

props에 isFoo 같은 플러그가 있다면, 컴포넌트 분리 검토를 권한다.