저는 함수형 컴포넌트
가 함수형 프로그래밍이나 뭐 그런 개념이 아니라 型
의 의미로 사용되어도 무방하다고 생각하다는 주의이기 때문에 함수형 컴포넌트
라는 용어를 사용하고, 이 글에서도 그렇게 하고자 합니다.
클래스 컴포넌트는 React
의 레거시라고 볼 수 있습니다.
레거시가 아니라고 생각하더라도 한 프로젝트에 두개를 섞어 쓰지 마세요.
인터넷에서 복사한 소스가 클래스 컴포넌트라면, 함수형 컴포넌트로 변환해서 프로젝트에 붙여넣으세요. 얼마 시간이 걸리지 않습니다.
memo
사용을 고려해보세요세상에는 (아마도) 모든 컴포넌트에 memo
를 사용해야한다고 생각하는 개발자가 10%, 선택적으로 사용해야한다고 생각하는 개발자가 90% 있는 것 같습니다.
저는 소수파에 속하지만, 아래 글도 한번 읽어 보시고 적극적인 memo
사용을 한번 고려해 보세요. memo
는 적용할 수 있는 가장 쉬운 최적화입니다.
https://stackoverflow.com/a/63405621/16303534
한가지 사견을 덧붙이자면, 리얼월드에서 deep-comparision
은 생각하는거보다 느리지 않습니다. 데이터가 다르면 두 오브젝트의 레퍼런스부터 다를(!==
) 확률이 높기 때문이죠.
then()
대신 async/await
를 사용하세요위의 함수형 컴포넌트
와 똑같은 주제입니다. then()
은 js 비동기 프로그래밍의 레거시입니다. 100% 레거시라기에는, await
도 내부적으로 then()
으로 동작하고, 호환성 문제도 있어서 JS 표준에서 then()
을 제거하는 선택지는 없을 것입니다.
사실 레거시 문제를 떠나서, 가장 중요한건 두가지 패턴을 섞지 마세요. then()
을 쓰셔도 됩니다만, 모든 코드에 then()
을 사용하시고, 인터넷에서 복사한 await
코드는 then()
으로 변환해서 사용하세요.
하지만 권장은 모든곳에 async/await
를 사용하는 것 입니다.
handleXXX()
대신 onXXX()
를 사용하세요. 그리고 통일하세요설명은 생략 하겠습니다.
함수
, 함수 컴포넌트
, 컴포넌트
사실 뭐라고 불러야 할지 저도 모르겠지만 아래 코드의 차이를 설명할 수 있나요?const renderItem = () => <MyItem />;
const renderItem2 = useMemo(() => <MyItem />, []);
<FoolatList
renderItem={renderItem}
renderItem={renderItem2}
renderItem={() => <MyItem />}
renderItem={MyItem}
/>
React.Fragment
와 <></>
의 차이점에 대해서 설명할 수 있나요?useEffect
내부에서 비동기 작업을 처리할 수 있나요?useState
대신 useRef
를 사용해 함수형 을 포기하는 대신 성능을 올리는 방법에 대해 알고 있나요?아래 내용들은 제 사견이 많이 포함된 주제들입니다.
그러니까 쉬운 것들
에서 둘중에 뭘 써라는 논란의 여지가 없지만, 쓸거면 둘중에 하나만 써라는 많은 분들이 동의하실것 같습니다.
아래 내용들은 그것보단 좀 더 편향적입니다. 감안해서 봐주세요.
useEffect
는 선택이 아닙니다.아래와 같은 캘린더 컴포넌트가 있고, 선택된 날짜, 혹은 달에 따라서 일정을 보여주는 스펙이 있다고 가정해보겠습니다.
코드로 구현하면 아래와 같이 짤 수 있을 것 입니다.
1번 코드 (X)
const loadData = async (date: DateTime) => {
setData(await fetch(`/foo?date=${date.toISO()}`));
};
return (
<Calendar
onClickPrevMonth={(date) => loadData(date)}
onClickNextMonth={(date) => loadData(date)}
onClickDate={(date) => loadData(date)}
onChange={setDate}
/>
);
2번 코드 (X)
const [date, setDate] = useState(DateTime.local());
const loadData = async (date: DateTime) => {
setData(await fetch(`/foo?date=${date.toISO()}`));
};
return (
<Calendar
onChange={(date) => {
setDate(date);
loadData(date);
}}
/>
);
3번 코드 (정답)
const [date, setDate] = useState(DateTime.local());
const loadData = async (date: DateTime) => {
setData(await fetch(`/foo?date=${date.toISO()}`));
};
useEffect(() => {
loadData();
}, [date]);
return (
<Calendar
onChange={setDate}
/>
);
정답과 오답을 나열해놓으니, 어떤점이 문제인지, 3번이 왜 정답인지는 꽤 명확해 보입니다만, 1, 2번 코드는 실제로 꽤 많이 본적이 있어서 이 항목을 작성했습니다.
useEffect
는 상태와 액션을 바인딩하는 아주 좋은 솔루션입니다.
기존의 jQuery식(사실 뭐든지) 개발방법이
이벤트
-> 액션
-> 상태변경
의 플로우였다면
React는
이벤트
-> 상태변경
-> 액션
-> 상태변경
의 순서로 동작합니다.
이 플로우를 항상 염두에 두고 컴포넌트를 만들어 주세요.
상태 관리 라이브러리를 더 적극적으로 사용하세요.
상태 관리 라이브러리는 단순히 여러개 상태값들의 집합이 아닙니다.
상태와, 상태 변경을 수행하는 액션들, 네트워킹, 써드 파티 라이브러리 처리 역시 포함할 수 있습니다.
아래 코드는 내 주변의 친구들을 가져오는 간단한 예제입니다.
FriendList.tsx
// 예외 처리는 생략합니다.
import { getDeviceLocation } from 'some-location-module';
const [isLoading, setIsLoading] = useState(false);
const [friends, setFriends] = useState();
useEffect(() => {
(async () => {
setIsLoading(true);
const location = await getDeviceLocation();
setFriends(
await fetch(`GET_NEARBY_FRIENDS_API?lat=${location.lat}&lng=${location.lng}`),
);
setIsLoading(false);
})();
}, []);
if (isLoading) {
return (
<div>
로딩중입니다.
</div>
);
}
return (
<>
{friends.map(x => (
<FriendItem
key={x.id}
data={x}
/>
)}
</>
);
사실 위 코드는 React 의 네트워킹 예제로써 아무 문제가 없습니다. 실제로 위 코드는 React Native 공식 문서 에도 저렇게 되어있습니다.
그래서 인터넷에는 정말 많은 위와 똑같이 생긴 예제가 있고, 강좌들이 있고, 프로덕트 코드들이 있습니다.
프로덕트 코드는 저렇게 짜면 안됩니다. 많은 사람들이 리액트를 잘못 짜는 이유중 가장 큰 부분은, 복사해서 붙여넣으면 안되는 폭탄같은 예제들이 너무 많이 돌아다닌다는 것입니다.
위 코드의 문제는 다음과 같습니다.
getDeviceLocation
호출에만 10줄정도를 할당해야 할 것입니다.useEffect
만 5~6개가 될 가능성이 높습니다.저는 실제로 컴포넌트 body(시작부터 ~ return까지)가 300줄 가량 되는 코드를 본 적이 있습니다.
아래 코드는 상태관리 라이브러리(여기선 mobx
)를 이용해 이 문제를 해결하는 방법을 보여줍니다.
friendStore.js
import { getDeviceLocation } from 'some-location-module';
const friendStore = observable({
friends: null,
isLoading: false,
loadFriends() {
this.isLoading = true;
// 중요: 써드파티 라이브러리 또한 제발 컴포넌트가 아니라 여기서 불러주세요.
const location = await getDeviceLocation();
this.friends = await fetch(`GET_NEARBY_FRIENDS_API?lat=${location.lat}&lng=${location.lng}`);
this.isLoading = false;
},
});
FriendList.tsx
const { friendStore } = useStores();
useEffect(() => {
friendStore.loadFriends();
}, []);
if (friendStore.isLoading) {
return (
<div>
로딩중입니다.
</div>
);
}
return (
<>
{friends.map(x => (
<FriendItem
key={x.id}
data={x}
/>
)}
</>
);
훨씬 나아졌습니다.
UI 코드와 UI가 아닌 코드가 깔끔하게 분리되었으며, loadFriends
의 재사용 또한 가능합니다. friends
레벨에서의 재사용도 가능합니다.
하지만 위 코드는 여전히 사소한 문제들이 있기 때문에, 한번 더 개선이 가능합니다.
result
만 리턴하는게 아니라 거의 필수적으로 isLoading
, error
, refetch
값들을 추가로 필요로 합니다.isLoadingFoo
, isLoadingBar
, isLoadingZoo
를 늘리실건가요?if(isLoading) <div>로딩중입니다.</div>
역시 프로덕션에서 사용할 수 없는 코드입니다.가장 좋은 해결방법은 각 요청들에 대해 hook
을 만드는것 입니다. 결과적으론 아래와 같은 코드가 가장 좋습니다.
apollo
혹은 useSWR
등의 라이브러리를 한번 시도해 보세요.
const {
data: friends,
isLoading,
error,
refetch,
} = useNearbyFriends();
// 더 좋은 로딩 처리는
// React.Suspense에서 힌트를 얻으실 수 있습니다.
몇가지 흥미로운 주제들이 더 있을 것 같지만, 글이 너무 길어져서 여기서 맺어야 할 것 같습니다.
React는 다루기 쉽지만, 잘못 짰을 경우 Angular, Vue 보다 느리게 동작합니다.
라이브러리 자체의 렌더링 퍼포먼스가 느리다는것이 아닙니다. 다른 라이브러리보다 잘못짜기 더 쉽고, 그렇기때문에 더 느리게 동작합니다.
다음에는 리액트를 느리게 만드는 것들(재실행)
에 대해 알아보도록 하겠습니다.