스마트폰 뒤로 가기 버튼을 앱이 종료할 때까지 눌러서 앱을 종료시킨 다음, 다시 처음으로 앱을 실행시킬 때만 '앱 잠금 해제' 화면이 뜰 수 있도록 하려면 어떻게 해야 할까요.
두 가지 요소가 필요했습니다.
1번을 해결하기 위해서는 리액트 네이티브 API인 AppState 코드를 활용합니다.
2번의 경우 리액트 네이티브 커뮤니티의 AsyncStorage 라이브러리를 사용해서 해결했습니다. project/storage/launchStorage.js 파일을 생성한 후, KEY 값이 firstLaunch인 AsyncStorage를 사용했죠.
그럼 1번 먼저 알아보고, 2번의 구체적인 사용 예시를 함께 살펴보겠습니다.
AppState는 앱이 foreground인지 또는 background인지 알려주고, 앱의 상태가 변경되면 알려주는 API입니다.
리액트 네이티브 공식 문서에서 이야기하는 App States로는 active, background, inactive(iOS) 세 가지가 있습니다.
foreground에서 실행 중임.background에서 실행 중임. 유저가AppState 메소드인 addEventListner 함수는 EventSubscription 객체를 반환하는데, 이 객체를 콘솔로 찍어 보면 'remove' 함수만 갖고 있다는 것을 알 수 있습니다.
앱이 background 상태였다가 foreground 상태로 바뀌었을 때 보게 되는 화면은, 앱을 종료시켰다가 다시 켰을 때 화면이기 때문에 당연히 앱의 첫 화면일 것입니다. 제가 만든 앱은 실행시켰을 때 첫 화면이 HomeScreen.js 스크린 인데요. 이 스크린의 가장 바깥 쪽을 <AppStateManager></AppStateManager> 태그로 감싸 AppStateManager.js 파일 내 코드가 적용되도록 했습니다.
AppStateManager.js 파일에서 AppState API를 사용합니다. 코드를 보겠습니다.
function AppStateManager({children}) {
const appState = useRef(AppState.currentState);
const [appStateVisible, setAppStateVisible] = useState(AppState.currentState);
// 앱의 상태가 inactive, background로 바뀔 때 호출하는 logout 함수입니다.
const inactiveLogout = async () => {
const auth = await FindAll();
logout(auth[0].address);
};
useEffect(() => {
/** 앱의 상태가 바뀔 때 동작하는 이벤트 함수를 등록함. */
const subscription = AppState.addEventListener('change', nextAppState => {
// 앱의 상태가 inactive, background에서 active로 바뀔 때.
if (
(appState.current === 'inactive' || appState.current === 'background')
&& nextAppState === 'active'
) {
/** (1) */
// launchStorage.set(true);
} else if (
// 앱의 상태가 active에서 inactive, background로 바뀔 때.
appState.current === 'active' &&
(nextAppState === 'inactive' || nextAppState === 'background')
) {
launchStorage.set(false);
inactiveLogout();
}
// appState.current 변수를 다음 상태로 업데이트 함.
appState.current = nextAppState;
setAppStateVisible(AppState.currentState);
});
return () => {
/** (2) 이벤트를 해제함. */
subscription.remove();
};
}, []);
if (appStateVisible == 'active') {
return <>{children}</>;
}
}
(1) 개발하면서 발견했는데, 앱을 종료했다가 다시 실행시켜도 첫번째 if문 내부 코드는 실행되지 않았습니다. (아직 이유를 찾지 못했습니다.)
(2) 자바스크립트 Publish/Subscription 패턴.
AsyncStorage는 리액트 네이티브에서 사용할 수 있는 key-value 형식의 저장소입니다.
AsyncStorage는 브라우저에서 사용하는 localStorage와도 꽤 비슷합니다. 값을 저장할 때는 문자열 타입으로 저장해야 하며, getItem, setItem, removeItem, clear 등 localStorage에서 사용하는 메서드와 같은 이름을 가진 메서드들도 존재합니다. localStorage와의 큰 차이점이라면 AsyncStorage는 비동기적으로 작동한다는 점입니다. 값을 조회하거나 설정할 때 Promise를 반환합니다.
리액트 네이티브로 File Manager 비슷한 앱을 개발하면서, AsyncStorage를 사용하면 더 좋았을 뻔한 상황들이 많았는데요. 그때마다 SQLite를 사용했고,
앱 환경 설정(Settings)같이 여러 Row가 저장되지 않는 테이블의 경우도 Row가 한 개뿐인 테이블로 생성하며 저장하곤 했습니다😂. 이 부분은 이렇게 처리하는 게 익숙해져서 개선을 위한 리팩토링은 숙제로 남아있습니다.
AsyncStorage를 사용하는 코드를 추상화 시켜 놓고 사용했습니다. 코드를 보겠습니다.
import AsyncStorage from '@react-native-community/async-storage';
const KEY = 'firstLaunch';
const launchStorage = {
async get() {
try {
const rawLaunch = await AsyncStorage.getItem(KEY);
const savedLaunch = JSON.parse(rawLaunch);
return savedLaunch;
} catch (e) {
throw new Error('Failed to load firstLaunch');
}
},
async set(data) {
try {
await AsyncStorage.setItem(KEY, JSON.stringify(data));
} catch (e) {
throw new Error('Failed to save firstLaunch');
}
},
};
export default launchStorage;