React-Native에서 스토리지를 다루기 위해 사용하는 두 가지 라이브러리에 대해서 정리해보려고 한다.
바로 Async-storage와 Encrypted-storage이다.
스토리지(Storage)를 왜 써야 하는지, 또 어떤 데이터를 넣어야 하며, redux와는 활용 측면에서 어떻게 비교가 되는지 정리해 보자.
Redux의 store는 데이터 저장 공간으로 활용되며 실제로 앱이 켜져있는 상태에서 데이터를 불러오는 성능이 가장 뛰어나다. 하지만 앱이 꺼지면 데이터가 사라지는 일시적 저장 공간이기 때문에 그 한계 또한 명확하다. (컴퓨터로 치면 RAM 같은 존재..)
따라서 데이터를 유지해 줄 공간이 필요하다. 앱이 꺼졌다 켜진 후에도 데이터 유지가 가능한 것이 바로 Async-storage이며, 웹으로 치면 로컬 스토리지와 유사하다.
그렇다면 Encrypted-storage는 왜 필요할까?
그 이유는 Async-storage 공식 문서에서 찾아볼 수 있다. 공식 문서에서 Async-storage는 'An asynchronous, unencrypted, persistent, key-value storage system for RN'이라고 정의되어 있다.
그대로 해석하면, '비동기적 - 암호화되지 않으며 - '키-값'으로 저장되는 시스템'이다. 데이터가 암호화되지 않는 스토리지이기 때문에 누구든지 값을 열어볼 수 있어 보안이 중요한 토큰을 보관할 시에는 적합하지 않다.
이를 보완한 것이 바로 Encrypted-storage이다. '비동기적 - '키-값'으로 저장'은 동일하며 실제로 구현 방식도 async-storage와 동일하다. 공식 문서의 한 줄 소개만 보아도 Async storage의 약점인 보안을 강화시키기 위한 라이브러리임을 알 수 있다. 따라서, 보안이 철저히 필요한 토큰들은 Async storage가 아닌 Encrypted storage에 넣어주면 된다.
정리하면 다음과 같다.
이제 구현 방식에 대해 정리해보자. 참고로 설치 방법은 아래의 공식 문서 링크를 참고하면 된다.
일반적으로 사용되는 메서드는 저장(setItem), 불러오기(getItem), 삭제(removeItem) 정도이다.
다음은 라이브러리에서 AsyncStorage를 가져와서 내부 함수들을 불러오는 방식으로 구현한다.
// 스토리지에 저장하기 - setItem
const storeData = async (value) => {
try {
const jsonValue = JSON.stringify(value) // 문자열로 변환하여 넣어주기 위함
await AsyncStorage.setItem('key', jsonValue)
} catch (e) {
// saving error
}
}
이때 반드시 기억해야할 점은 키-값 형태로 보관됨에 있어 저장되는 값은 무조건 문자열로 취급되어야 한다. 따라서, 저장할 때에는 JSON.stringify()를 써주고, 가져올 때에는 JSON.parse()를 사용해서 구현해야 한다.
또한, 비동기 처리를 위하여 async/await과 에러 핸들링을 위해 try-catch 문을 써준다. key 값이 공백 또는 null 일 때에는 error를 throw 해주고, value가 존재하지 않는다면 null을 반환하도록 한다.
// 스토리지에서 값 불러오기 - getItem
const getData = async () => {
try {
const jsonValue = await AsyncStorage.getItem('key')
return jsonValue != null ? JSON.parse(jsonValue) : null;
} catch (e) {
// error reading value
}
}
아래 코드는 로그인할 때에 반환되는 토큰들을 스토리지에 넣어주는 코드이다. 동일하게 비동기를 위한 async/await과 에러 핸들링을 위한 try-catch문을 사용하고, 이 경우 refreshToken을 보안을 위하여 Encrypted storage에 setItem 함수를 사용하여 넣어준다.
import EncryptedStorage from 'react-native-encrypted-storage';
const onSubmit = useCallback(async () => {
if (loading) {
return;
}
try {
setLoading(true);
const response = await axios.post(`${Config.API_URL}/login`, {
email,
password,
});
dispatch(
userSlice.actions.setUser({
name: response.data.name,
email: response.data.email,
accessToken: response.data.accessToken,
}),
);
await EncryptedStorage.setItem(
'refreshToken',
response.data.refreshToken,
);
} catch (error) {
const errorResponse = (error as AxiosError).response;
if (errorResponse) {
Alert.alert(errorResponse.data.message);
}
} finally {
setLoading(false);
}
}, [dispatch, email, loading, password]);
하단의 코드는 스토리지에서 토큰을 지우기 위하여 removeItem 함수를 사용한 예제이다.
로그아웃한 사용자는 저장공간에 토큰이 존재하면 안되기 때문에 제거해줘야 한다.
import EncryptedStorage from 'react-native-encrypted-storage';
const onLogout = useCallback(async () => {
try {
await axios.post(
`${Config.API_URL}/logout`,
{},
{
headers: {
authorization: `Bearer ${accessToken}`,
},
},
);
Alert.alert('알림', '로그아웃 되었습니다.');
dispatch(
userSlice.actions.setUser({
name: '',
email: '',
accessToken: '',
}),
);
await EncryptedStorage.removeItem('refreshToken');
} catch (error) {
const errorResponse = (error as AxiosError).response;
console.error(errorResponse);
}
}, [accessToken, dispatch]);
👍