알림 서비스를 사용하기 위해서는 Push Token 발급이 필요하다. 우리가 새로운 앱을 다운 받았을 때 알림을 동의하는지에 대한 여부를 물어보는 것을 본 적이 있을 것이다. 이것이 아마 정책에 따른 것이라 판단되어지며, 알림을 동의했을 경우 Push Token 이 발급되어 해당 유저에게는 알림 기능이 작동하고, 동의하지 않았을 경우 Push Token 이 발급되지 않아 알림 기능이 작동하지 않을 것이다. 따라서 이번 글에서는 Push Token 을 발급하는 과정을 작성하고, 개인적으로 구축한 DB 에 해당 Push Token 이 저장되어 관리될 수 있게 해보기로 한다.
1. 의존성

EXPO 에서 알림 기능을 위한 Push Token 을 발급받기 위해서는 expo-noritifications 가 필요하다. EXPO 와 관련된 글을 검색하다 보면 expo-permissions 도 필요하다고 되어 있다. expo-permissions 의 경우 푸시 알림, 카메라, 위치 정보 등 기기 권한을 확인하고 요청할 때 사용되어졌는데 이 의존성은 EXPO SEK 43 버전 까지만 사용되어지고 이후 버전에서는 각 기능별 패키지가 자체적으로 권한을 처리한다. 따라서 EXPO SDK 52 를 사용하고 있는 필자의 경우 해당 의존성은 필요하지 않다.
2. Push Token 을 발급 받기 위한 조건 및 EXPO 코드
Push Token 발급은 실제 기기에서만 작동한다. 굉장히 불친절하다. 필자의 경우 처음 웹 에서 해당 토큰을 발급받으려 했지만 웹 에서 토큰을 발급받기 위해서는 Web Push API 를 사용해야 한다. 또, 다음으로 이를 해결하기 위해 Xcode 를 활용한 iOS 시뮬레이터를 사용하였지만 이 또한 실제 기기가 아니기 때문에 발급이 불가능 하였다. 따라서 어쩔 수 없이 실제 기기를 활용해야 하는 조건 속에서 문제를 해결하기로 하였다.
2.1 registerMobilePush
async function registerMobilePush() {
if (!Device.isDevice) {
alert("푸시 알림은 실제 기기에서만 지원됩니다.");
return null;
}
const {status: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== "granted") {
const { status } = await Notifications.requestPermissionsAsync();
console.log("알림 권한 상태",status);
finalStatus = status;
}
if (finalStatus !== "granted") {
alert("푸시 알림 권한이 거부되었습니다.");
return null;
}
const token = (await Notifications.getExpoPushTokenAsync()).data;
console.log("📌 Expo Push Token (모바일):", token);
return token;
}
앞서 얘기했듯 푸시 알림은 실제 기기에서만 지원된다는 문구가 있다. 따라서 에뮬레이터로 실행할 경우 해당 문구가 뜨면서 Push Token 발급이 진행되지 않는다. getPermissionsAsync 에는 "granted", "denied", "undetermined" 총 세가지의 권한 상태가 존재한다. 이는 각각 허용, 거부, 최초 요청 전 상태 로 구분되어진다. 이 때, 알림 권한 상태가 "granted" 일 경우에만 token 에 해당 Push Token 값을 저장하여 반환한다.

2.2 Push Token 을 가져와 서버에 저장하는 역할
export async function getPushToken() {
const userId = useUserStore.getState().user?.userId;
if (!userId) {
console.warn("Could not find userId");
return;
}
let token;
if (Platform.OS === "web") {
token = await registerWebPush();
} else {
token = await registerMobilePush();
}
if (token) {
await sendPushTokenToServer(userId, token);
}
}
해당 코드는 사용자의 Push Token 을 가져와 서버에 저장하는 역할을 한다. 웹 환경과 모바일 환경을 구분하여 Push Token 을 발급받을 수 있게 구현하였다. 발급 받은 Push Token 은 서버로 전송하여 저장한다. 상태 관리 라이브러리 (Zustand) 를 사용하여 현재 로그인한 사용자의 userId 를 가지고 온다. userId 가 없다면 로그인이 안된 상태 이기 때문에, Push Token 을 받을 수 없다. 리턴받은 token 값과 상태 관리 라이브러리에 저장된 userId 를 가지고 sendPushTokenServer 로 요청한다.
2.3 sendPushTokenToServer
async function sendPushTokenToServer(userId: string, token: string) {
try {
const response = await axios.post(API_URL, {
userId,
token,
});
console.log("✅ 서버 응답:", response.data);
} catch (error) {
console.error("🚨 서버 전송 오류:", error);
}
}
userId 와 token 값을 가지고 해당 값을 저장하기 위한 백엔드로 전송한다. 이 때 EXPO 와 알림 서비스의 REST API 를 통해 백엔드로 전달한다.
3. Push Token 을 저장하기 위한 Spring Boot 코드
Expo 에서 발급받은 Push Token 을 저장하기 위해서 백엔드 로직이 필요하다. 보통 해당 토큰을 저장하기 위해 Firebase 를 사용하지만 필자의 경우 직접 구축한 Redis 에 저장하여 따로 관리할 수 있도록 하였다.
3.1 RedisConfig
@Configuration
public class RedisConfig {
private final String redisHost = System.getenv("REDIS_HOST");
private final int redisPort = Integer.parseInt(System.getenv("REDIS_PORT"));
private final String redisPassword = System.getenv("REDIS_PASSWORD");
private final int redisDatabase = Integer.parseInt(System.getenv("REDIS_DATABASE"));
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(redisHost, redisPort);
config.setPassword(redisPassword);
config.setDatabase(redisDatabase);
return new LettuceConnectionFactory(config);
}
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory connectionFactory) {
return new StringRedisTemplate(connectionFactory);
}
}
발급받은 Push Token 을 Redis 에 저장하기 위해 Redis 설정을 하였다. Redis 의 0번 데이터베이스는 이미 채팅서비스에서 오프라인 판별 유저를 판단하기 위한 목적으로 사용되어지고 있다. 따라서 독립적으로 관리하기 위하여 다른 데이터베이스를 사용하게끔 명시하였다. 또한, 단순 문자열 데이터로 저장되기 때문에 stringRedisTemplate 를 bean 으로 등록하였다.
3.2 PushTokenController
@RestController
@RequestMapping("/push-token")
public class PushTokenController {
private final PushTokenService pushTokenService;
public PushTokenController(PushTokenService pushTokenService) {
this.pushTokenService = pushTokenService;
}
@PostMapping("/register")
public String registerPushToken(@RequestBody Map<String, String> request) {
String userId = request.get("userId");
String token = request.get("token");
pushTokenService.savePushToken(userId, token);
return "Push token registered successfully";
}
}
Push Token 요청을 처리할 Controller 이다. 해당 값을 Map 으로 관리한다.
3.3 PushTokenService
@Service
public class PushTokenService {
private final StringRedisTemplate redisTemplate;
public PushTokenService(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
public void savePushToken(String userId, String token) {
redisTemplate.opsForValue().set("push_token: " + userId, token, 30, TimeUnit.DAYS);
}
public String getPushToken(String userId) {
String token = redisTemplate.opsForValue().get("push_token: " + userId);
if (token == null) {
System.err.println("PushTokenService: pushToken is null");
}
return token;
}
}
Controller 에서 전달받은 userId 와 token 을 Redis 에 저장하기 위한 비즈니스 로직이다. 전달받은 토큰값은 opsForValue 를 사용하여 Key, Value 형태로 저장한다. Push Token 은 30일 후 자동으로 삭제되게 설정하였다. 이후 EXPO 에 알림 전송시 포함할 Push Token 값을 조회할 수 있는 로직도 구현되어 있다.
3.4 ExpoPushService
public class ExpoPushService {
private static final String EXPO_PUSH_URL = "https://exp.host/--/api/v2/push/send";
private final RestTemplate restTemplate = new RestTemplate();
public boolean sendPushNotification(String pushToken, String roomId, String userId, String message, String roomName) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
String body = userId + ": " + message;
Map<String, Object> payload = new HashMap<>();
payload.put("to", pushToken);
payload.put("title", roomName);
payload.put("body", body);
payload.put("sound", "default");
HttpEntity<Map<String, Object>> request = new HttpEntity<>(payload, headers);
try{
ResponseEntity<String> response = restTemplate.exchange(EXPO_PUSH_URL, HttpMethod.POST, request, String.class);
if (!response.getStatusCode().is2xxSuccessful()) {
System.err.println("Expo Push Notification ERROR: " + response.getBody());
}
return response.getStatusCode().is2xxSuccessful();
} catch (Exception e) {
System.err.println("Expo Push Notification ERROR: " + e.getMessage());
return false;
}
}
}
EXPO 에서는 서버를 직접 설정하지 않아도 푸시 알림을 보내는 중앙 서버를 제공한다. 따라서 EXPO_PUSH_URL 은 개발 환경과 배포 환경 모두에서 사용 가능하다. 알림의 경우 누가, 언제, 어디로, 무엇을 보냈는지가 필수조건이다. payload 에 저장된 데이터를 HttpEntity 객체로 묶는다. 최종적으로 EXPO_PUSH_URL 에 POST 로 JSON 형태의 Body 를 전송한다.

'Project > ST00CK' 카테고리의 다른 글
EXPO -> React Vite 로 리펙토링 (추후 방향성) (0) | 2025.03.31 |
---|---|
gRPC 와 REST API 서버 동시에 사용하기 (0) | 2025.03.04 |
Redis 를 활용한 오프라인 유저 찾기 로직 (0) | 2025.02.27 |
KafkaConsumer 중복 실행 오류 수정 (0) | 2025.02.26 |
socket.io 방 나가기 기능 및 연결 해제 처리 (0) | 2025.02.25 |
알림 서비스를 사용하기 위해서는 Push Token 발급이 필요하다. 우리가 새로운 앱을 다운 받았을 때 알림을 동의하는지에 대한 여부를 물어보는 것을 본 적이 있을 것이다. 이것이 아마 정책에 따른 것이라 판단되어지며, 알림을 동의했을 경우 Push Token 이 발급되어 해당 유저에게는 알림 기능이 작동하고, 동의하지 않았을 경우 Push Token 이 발급되지 않아 알림 기능이 작동하지 않을 것이다. 따라서 이번 글에서는 Push Token 을 발급하는 과정을 작성하고, 개인적으로 구축한 DB 에 해당 Push Token 이 저장되어 관리될 수 있게 해보기로 한다.
1. 의존성

EXPO 에서 알림 기능을 위한 Push Token 을 발급받기 위해서는 expo-noritifications 가 필요하다. EXPO 와 관련된 글을 검색하다 보면 expo-permissions 도 필요하다고 되어 있다. expo-permissions 의 경우 푸시 알림, 카메라, 위치 정보 등 기기 권한을 확인하고 요청할 때 사용되어졌는데 이 의존성은 EXPO SEK 43 버전 까지만 사용되어지고 이후 버전에서는 각 기능별 패키지가 자체적으로 권한을 처리한다. 따라서 EXPO SDK 52 를 사용하고 있는 필자의 경우 해당 의존성은 필요하지 않다.
2. Push Token 을 발급 받기 위한 조건 및 EXPO 코드
Push Token 발급은 실제 기기에서만 작동한다. 굉장히 불친절하다. 필자의 경우 처음 웹 에서 해당 토큰을 발급받으려 했지만 웹 에서 토큰을 발급받기 위해서는 Web Push API 를 사용해야 한다. 또, 다음으로 이를 해결하기 위해 Xcode 를 활용한 iOS 시뮬레이터를 사용하였지만 이 또한 실제 기기가 아니기 때문에 발급이 불가능 하였다. 따라서 어쩔 수 없이 실제 기기를 활용해야 하는 조건 속에서 문제를 해결하기로 하였다.
2.1 registerMobilePush
async function registerMobilePush() {
if (!Device.isDevice) {
alert("푸시 알림은 실제 기기에서만 지원됩니다.");
return null;
}
const {status: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== "granted") {
const { status } = await Notifications.requestPermissionsAsync();
console.log("알림 권한 상태",status);
finalStatus = status;
}
if (finalStatus !== "granted") {
alert("푸시 알림 권한이 거부되었습니다.");
return null;
}
const token = (await Notifications.getExpoPushTokenAsync()).data;
console.log("📌 Expo Push Token (모바일):", token);
return token;
}
앞서 얘기했듯 푸시 알림은 실제 기기에서만 지원된다는 문구가 있다. 따라서 에뮬레이터로 실행할 경우 해당 문구가 뜨면서 Push Token 발급이 진행되지 않는다. getPermissionsAsync 에는 "granted", "denied", "undetermined" 총 세가지의 권한 상태가 존재한다. 이는 각각 허용, 거부, 최초 요청 전 상태 로 구분되어진다. 이 때, 알림 권한 상태가 "granted" 일 경우에만 token 에 해당 Push Token 값을 저장하여 반환한다.

2.2 Push Token 을 가져와 서버에 저장하는 역할
export async function getPushToken() {
const userId = useUserStore.getState().user?.userId;
if (!userId) {
console.warn("Could not find userId");
return;
}
let token;
if (Platform.OS === "web") {
token = await registerWebPush();
} else {
token = await registerMobilePush();
}
if (token) {
await sendPushTokenToServer(userId, token);
}
}
해당 코드는 사용자의 Push Token 을 가져와 서버에 저장하는 역할을 한다. 웹 환경과 모바일 환경을 구분하여 Push Token 을 발급받을 수 있게 구현하였다. 발급 받은 Push Token 은 서버로 전송하여 저장한다. 상태 관리 라이브러리 (Zustand) 를 사용하여 현재 로그인한 사용자의 userId 를 가지고 온다. userId 가 없다면 로그인이 안된 상태 이기 때문에, Push Token 을 받을 수 없다. 리턴받은 token 값과 상태 관리 라이브러리에 저장된 userId 를 가지고 sendPushTokenServer 로 요청한다.
2.3 sendPushTokenToServer
async function sendPushTokenToServer(userId: string, token: string) {
try {
const response = await axios.post(API_URL, {
userId,
token,
});
console.log("✅ 서버 응답:", response.data);
} catch (error) {
console.error("🚨 서버 전송 오류:", error);
}
}
userId 와 token 값을 가지고 해당 값을 저장하기 위한 백엔드로 전송한다. 이 때 EXPO 와 알림 서비스의 REST API 를 통해 백엔드로 전달한다.
3. Push Token 을 저장하기 위한 Spring Boot 코드
Expo 에서 발급받은 Push Token 을 저장하기 위해서 백엔드 로직이 필요하다. 보통 해당 토큰을 저장하기 위해 Firebase 를 사용하지만 필자의 경우 직접 구축한 Redis 에 저장하여 따로 관리할 수 있도록 하였다.
3.1 RedisConfig
@Configuration
public class RedisConfig {
private final String redisHost = System.getenv("REDIS_HOST");
private final int redisPort = Integer.parseInt(System.getenv("REDIS_PORT"));
private final String redisPassword = System.getenv("REDIS_PASSWORD");
private final int redisDatabase = Integer.parseInt(System.getenv("REDIS_DATABASE"));
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(redisHost, redisPort);
config.setPassword(redisPassword);
config.setDatabase(redisDatabase);
return new LettuceConnectionFactory(config);
}
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory connectionFactory) {
return new StringRedisTemplate(connectionFactory);
}
}
발급받은 Push Token 을 Redis 에 저장하기 위해 Redis 설정을 하였다. Redis 의 0번 데이터베이스는 이미 채팅서비스에서 오프라인 판별 유저를 판단하기 위한 목적으로 사용되어지고 있다. 따라서 독립적으로 관리하기 위하여 다른 데이터베이스를 사용하게끔 명시하였다. 또한, 단순 문자열 데이터로 저장되기 때문에 stringRedisTemplate 를 bean 으로 등록하였다.
3.2 PushTokenController
@RestController
@RequestMapping("/push-token")
public class PushTokenController {
private final PushTokenService pushTokenService;
public PushTokenController(PushTokenService pushTokenService) {
this.pushTokenService = pushTokenService;
}
@PostMapping("/register")
public String registerPushToken(@RequestBody Map<String, String> request) {
String userId = request.get("userId");
String token = request.get("token");
pushTokenService.savePushToken(userId, token);
return "Push token registered successfully";
}
}
Push Token 요청을 처리할 Controller 이다. 해당 값을 Map 으로 관리한다.
3.3 PushTokenService
@Service
public class PushTokenService {
private final StringRedisTemplate redisTemplate;
public PushTokenService(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
public void savePushToken(String userId, String token) {
redisTemplate.opsForValue().set("push_token: " + userId, token, 30, TimeUnit.DAYS);
}
public String getPushToken(String userId) {
String token = redisTemplate.opsForValue().get("push_token: " + userId);
if (token == null) {
System.err.println("PushTokenService: pushToken is null");
}
return token;
}
}
Controller 에서 전달받은 userId 와 token 을 Redis 에 저장하기 위한 비즈니스 로직이다. 전달받은 토큰값은 opsForValue 를 사용하여 Key, Value 형태로 저장한다. Push Token 은 30일 후 자동으로 삭제되게 설정하였다. 이후 EXPO 에 알림 전송시 포함할 Push Token 값을 조회할 수 있는 로직도 구현되어 있다.
3.4 ExpoPushService
public class ExpoPushService {
private static final String EXPO_PUSH_URL = "https://exp.host/--/api/v2/push/send";
private final RestTemplate restTemplate = new RestTemplate();
public boolean sendPushNotification(String pushToken, String roomId, String userId, String message, String roomName) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
String body = userId + ": " + message;
Map<String, Object> payload = new HashMap<>();
payload.put("to", pushToken);
payload.put("title", roomName);
payload.put("body", body);
payload.put("sound", "default");
HttpEntity<Map<String, Object>> request = new HttpEntity<>(payload, headers);
try{
ResponseEntity<String> response = restTemplate.exchange(EXPO_PUSH_URL, HttpMethod.POST, request, String.class);
if (!response.getStatusCode().is2xxSuccessful()) {
System.err.println("Expo Push Notification ERROR: " + response.getBody());
}
return response.getStatusCode().is2xxSuccessful();
} catch (Exception e) {
System.err.println("Expo Push Notification ERROR: " + e.getMessage());
return false;
}
}
}
EXPO 에서는 서버를 직접 설정하지 않아도 푸시 알림을 보내는 중앙 서버를 제공한다. 따라서 EXPO_PUSH_URL 은 개발 환경과 배포 환경 모두에서 사용 가능하다. 알림의 경우 누가, 언제, 어디로, 무엇을 보냈는지가 필수조건이다. payload 에 저장된 데이터를 HttpEntity 객체로 묶는다. 최종적으로 EXPO_PUSH_URL 에 POST 로 JSON 형태의 Body 를 전송한다.

'Project > ST00CK' 카테고리의 다른 글
EXPO -> React Vite 로 리펙토링 (추후 방향성) (0) | 2025.03.31 |
---|---|
gRPC 와 REST API 서버 동시에 사용하기 (0) | 2025.03.04 |
Redis 를 활용한 오프라인 유저 찾기 로직 (0) | 2025.02.27 |
KafkaConsumer 중복 실행 오류 수정 (0) | 2025.02.26 |
socket.io 방 나가기 기능 및 연결 해제 처리 (0) | 2025.02.25 |