개발 중이던 앱은 푸시 알림을 받아 웹뷰를 보여주는 기능을 수행한다. 이때 푸시 알림은 가장 상위단에서 수신하고 url 정보 갱신한 후 하위 Home까지 props로 전달하고, 최종적으로 Home.js에서 useState를 이용해 url 변수를 갱신하여 rerender하는 구조를 계획하였다. 하지만 useState의 남용에 의해 아래 에러가 발생하였다.
"Too many re-renders. React limits the number of renders to prevent an infinite loop."
해당 에러가 발생한 구조는 아래와 같다.
// App.js
import React, { useEffect, useState } from 'react';
import * as Notifications from 'expo-notifications';
const App = () => {
const [expoPushToken, setExpoPushToken] = useState("");
const [url, setUrl] = useState('https://github.com/sehoon787?tab=repositories');
registerForPushNotificationsAsync(); // 구현은 편의상 생략
useEffect(() => {
const fgNotificationResponse = Notifications.addNotificationReceivedListener(notification => {
setUrl(notification.request.content.data.url);
};
return(
<NavigationContainer>
<StackNavigation url={url}/>
</NavigationContainer>
);
};
export default App;
중요한 부분은 11번 line부터 있는 useEffect Hook을 이용해 setUrl을 통해 rerender를 위한 동작 수행 후 그 url을 하위로 전달하는 부분이다. 편의상 Navigator 코드는 생략하고 Home.js 코드이다.
// Home.js
import React, { useState } from "react";
import { WebView } from "react-native-webview";
import * as Notifications from 'expo-notifications';
const Home = ({route}) => {
const lastNotificationResponse = Notifications.useLastNotificationResponse(); // Hook
const [url, setUrl] = useState("");
if(lastNotificationResponse && lastNotificationResponse.notification.request.content.data.url){
setUrl(lastNotificationResponse.notification.request.content.data.url);
}
else {
setUrl(route.params.url.url);
}
return (
<WebView
style ={{marginTop :30,}}
source={{
uri: route.params.url.url
}}
/>
);
};
export default Home;
자세히 보아야 할 곳은 11번째 라인부터 있는 if 구문이다. 여기서 다시 또 setUrl을 하며 useState를 통해 render가 되는 페이지에 들어갈 요소들을 너무 많이 rerender가 되도록 야기한 것이 문제인 것으로 생각된다.
여기부터는 해결한 내용이다. 어짜피 핵심은 쓸데없이 useState를 남발하는 것을 피하는 것이 좋다는 것이다.
// App.js
import React, { useEffect, useState } from 'react';
import * as Notifications from 'expo-notifications';
import UrlContext from './contexts/Url';
const App = () => {
const [expoPushToken, setExpoPushToken] = useState("");
const [url, setUrl] = useState('https://github.com/sehoon787?tab=repositories');
registerForPushNotificationsAsync(); // 구현은 편의상 생략
useEffect(() => {
const fgNotificationResponse = Notifications.addNotificationReceivedListener(notification => {
setUrl(notification.request.content.data.url);
};
return(
<UrlContext.Provider value={{url: url}}>
<NavigationContainer>
<StackNavigation/>
</NavigationContainer>
</UrlContext.Provider>
);
};
export default App;
App.js는 props를 이용해 하위 컴포넌트와 뷰로 전달하는 방식보다 ContextAPI를 이용해 전역 변수를 관리하는 형태로 확장성을 고려해 재설계하였다. 또한 App.js에서 Home에 해당하는 view에 전달할 url값을 Provider를 이용해 관리 및 전달한다.
// Home.js
import React, { useState } from "react";
import { WebView } from "react-native-webview";
import * as Notifications from 'expo-notifications';
import UrlContext from '../contexts/Url';
const Home = ({route}) => {
const lastNotificationResponse = Notifications.useLastNotificationResponse(); // Hook
return (
<UrlContext.Consumer>{value=>
<WebView
style ={{marginTop :30,}}
source={{
uri: (lastNotificationResponse && lastNotificationResponse.notification.request.content.data.url) ?
lastNotificationResponse.notification.request.content.data.url : value.url
}}
/>}
</UrlContext.Consumer>
);
};
export default Home;
또한 Home에서 다시 useState를 통해 관리하려 했던 변수는 해당 useState line대신 삼항연산자를 이용해 바로 적용되도록 수정하여 해결하였다.
변수 상태 관리를 하기위한 함수형 프로그래밍에서 useState는 매우 유용하지만 rerender가 너무 빈번하게 일어날 수 있으므로 신중하게 사용할 필요가 있다. 이를 위해 전역적으로 변수 상태 관리를 수행하게 도와주며 계속 props를 통해 하위 컴포넌트로 전달하는 코드 수정을 줄여주는 ContextAPI, redux를 사용하는 것도 방법이 될 수 있을 것 같다.
또한 if문이나 추가 작업을 통해 수정될 수 있는 부분은 삼항 연산자(Ternary Operator) 등을 이용해 해결해볼 수 있다.