내자산: 마이데이터 자산 조회

(feat. WebSocket)

wonyong01.kim
NAVER Pay Dev Blog
12 min readDec 21, 2023

--

안녕하세요. 네이버페이 내자산&회원FE 팀의 김원용입니다.

저희 팀은 네이버페이 포인트는 물론, 마이데이터를 통한 다양한 금융 기관의 자산 정보를 한 곳에서 손쉽게 확인하고 관리할 수 있는, 네이버페이 내자산 서비스를 개발하고있습니다.

이 글에서는 네이버페이 내자산 서비스에 대한 간략한 소개와 함께, 마이데이터 자산 조회 개발 과정을 단계별로 설명해보려고 합니다!

네이버페이 내자산 서비스: https://new-m.pay.naver.com/mydata/home

네이버페이 내자산 서비스 (이하 “내자산”)네이버페이 자산마이데이터 자산 한 곳에서 조회 가능한 서비스 입니다.

네이버페이 자산:
- 네이버페이 포인트
- 네이버페이 머니(하나 통장)
- 미래에셋 네이버통장
- 후불결제
- 우리집
- 마이카

마이데이터 자산:
- 은행
- 카드
- 보험
- 증권
- 대출
- 연금/IRP

마이데이터는 고객이 본인의 개인신용정보를 금융회사로부터 마이데이터사업자에게 전송하도록 요구할 수 있는 권리를 의미합니다.

이를 통해 사용자는 네이버페이에서 다양한 금융회사에 등록된 자신의 개인신용정보를 한눈에 조회하고 효율적으로 관리할 수 있습니다.

여러 금융회사로부터 데이터를 가져온다는 개념을 바탕으로, 사용자의 모든 은행 계좌 잔액을 조회할 수 있는 은행 총 잔액 컴포넌트 구현해보겠습니다.

단순한 은행 총 잔액 컴포넌트
단순한 은행 총 잔액 컴포넌트

1. 하나의 API로 모든 은행 잔액 확인하기

은행 총 잔액을 표시하는 가장 간단한 방법은 한 번의 BE API 호출을 통해 데이터를 가져오는 것입니다.

BE 에서는 모든 금융회사의 잔액을 병렬로 조회한 후, 이를 합산하여 응답합니다.
BE 에서는 모든 금융회사의 잔액을 병렬로 조회한 후, 이를 합산하여 응답합니다.
// 은행 계좌의 총 잔액을 조회하는 함수
const fetchBankTotalBalance = async () => {
const bankTotalBalance = await fetch('/api/bank/total-balance');
return bankTotalBalance; // 예: 999,999,999
}

/* ================================================================== */

// 총 잔액 조회 및 화면에 표시
const bankTotalBalance = await fetchBankTotalBalance();
render(bankTotalBalance);

그러나, 만약 어떤 특정 금융기관의 데이터를 받아오는 데 시간이 많이 소요된다면, 이는 사용자 경험에 어떤 영향을 미칠까요?

금융회사C 응답이 60초가 걸린다면 … ?
금융회사C 응답이 60초가 걸린다면 … ?

2. 병렬 요청 방식

마이데이터 자산 데이터는 여러 금융기관으로부터 가져오기 때문에, 특정 기관의 응답 지연은 전체 데이터 처리 속도에 영향을 미칠 수 있습니다.
예를 들어, 금융회사 C의 응답이 지연될 경우, 사용자는 전체 잔액 정보를 볼 수 없게 됩니다.

이를 개선하기 위해 FE 에서는 금융회사별로 병렬 요청을 보내는 방식을 도입했습니다.

FE 에서 금융회사별로 병렬 요청합니다.
FE 에서 금융회사별로 병렬 요청합니다.
// 각 금융회사의 잔액 정보를 저장하는 객체
const bankBalanceInfo = {
금융회사A: null,
금융회사B: null,
금융회사C: null,
}

// 개별 은행 계좌의 잔액을 조회하는 함수
const fetchBankBalance = async (bankCode) => {
const bankBalance = await fetch(`/api/bank/balance/${bankCode}`);
return bankBalance; // 예: 100,000,000
}

// 금융사별 잔액 업데이트 및 화면에 표시
const updateAndRender = (bankCode, balance) => {
bankBalanceInfo[bankCode] = balance;
render(sum(bankBalanceInfo));
}

/* ================================================================== */

// 금융회사 병렬 요청
Object.keys(bankBalanceInfo).forEach(async (bankCode) => {
const bankBalance = await fetchBankBalance(bankCode);
updateAndRender(bankCode, bankBalance);
})

이 방식을 통해 빠르게 응답을 받은 금융회사의 잔액 정보부터 합산하여 화면에 표시하게 되므로, 사용자는 신속하게 전체 정보를 확인할 수 있습니다.

그러나, 만약 여러 금융기관에서 지연이 생기면, 이는 사용자 경험에 어떤 영향을 미칠까요?

금융회사A 만 빠르게 온다면 … ?
금융회사A 만 빠르게 온다면 … ?

3. 캐시된 정보 활용

여러 금융기관에서 데이터를 가져오는 과정에 지연이 생길 때, 병렬 요청 방식을 사용해도 전체 잔액 정보에 일시적인 오차가 생길 수 있습니다.
예를 들어, 전체 잔액이 999,999,999원인데 금융회사 A의 100,000,000원만 먼저 오면, 화면에는 100,000,000원만 표시되고, 지연 후에 서서히 999,999,999원으로 업데이트됩니다. 이것은 사용자에게 좋지 않은 경험을 줄 수 있습니다.

이를 개선하기 위해 BE 에서 마지막으로 조회한 금융회사 데이터를 저장하고, FE 에서는 이 저장된 잔액을 먼저 보여줌과 동시에 최신 잔액으로 업데이트하는 방식을 도입했습니다.

캐시된 잔액을 우선적으로 화면에 노출시키고, 병렬 요청 방식으로 금융회사의 최신 잔액으로 갱신합니다.
캐시된 잔액을 우선적으로 화면에 노출시키고, 병렬 요청 방식으로 금융회사의 최신 잔액으로 갱신합니다.
// 각 금융회사의 잔액 정보를 저장하는 객체
const bankBalanceInfo = {
금융회사A: null,
금융회사B: null,
금융회사C: null,
}

// 캐시된 총 잔액 정보를 먼저 가져오는 함수
const fetchCachedBankTotalBalance = async () => {
const cachedBankTotalBalance = await fetch('/api/bank/cached-total-balance');
return cachedBankTotalBalance; // 예: { 금융회사A: 100,000,000원, 금융회사B: 200,000,000원, ... }
}

// 개별 은행 계좌의 잔액을 조회하는 함수
const fetchBankBalance = async (bankCode) => {
const bankBalance = await fetch(`/api/bank/balance/${bankCode}`);
return bankBalance; // 예: 100,000,000
}

// 금융사별 잔액 업데이트 및 화면에 표시
const updateAndRender = (bankCode, balance) => {
bankBalanceInfo[bankCode] = balance;
render(sum(bankBalanceInfo));
}

/* ================================================================== */

// 먼저 캐시된 총 잔액 정보를 화면에 표시
const cachedBankTotalBalance = await fetchCachedBankTotalBalance();
render(sum(cachedBankTotalBalance));

// 금융회사 병렬 요청
Object.keys(bankBalanceInfo).forEach(async (bankCode) => {
const bankBalance = await fetchBankBalance(bankCode);
updateAndRender(bankCode, bankBalance);
})

이 방법을 통해 사용자는 더욱 신속하고 안정적으로 은행 잔액 정보를 확인할 수 있게 되었습니다!

4. WebSocket 적용으로 더 나아가기

지금까지 3개 금융회사의 은행 잔액 조회 예시를 살펴봤습니다. 그러나 “내자산” 서비스는 은행뿐만 아니라 카드, 증권 등 다양한 금융회사의 데이터 조회도 지원합니다. 이는 갱신을 위한 HTTP API 요청의 증가를 의미합니다.

자산이 100개라면 … ?
자산이 100개라면 … ?

이러한 상황을 개선하기 위해, Socket.IO 기반의 사내 플랫폼을 활용하여 WebSocket을 적용하였습니다.

WebSocket 과 Socket.io 에 대한 자세한 내용은 NAVER D2 포스팅에서 확인 가능합니다.

WebSocket은 한 번의 연결 설정으로 지속적인 데이터 교환을 가능하게 합니다. 이는 기존의 HTTP 요청 방식과 비교할 때 큰 이점을 제공합니다. HTTP 방식에서는 각 요청마다 헤더와 추가 데이터를 보내야 하지만, WebSocket은 초기 연결 설정 후에는 이러한 오버헤드 없이 데이터를 전송할 수 있습니다. 이는 특히 많은 양의 데이터를 실시간으로 주고받아야 하는 경우, 더욱 효율적입니다.

한 번의 갱신 요청 API를 통해 전체 금융회사에 대한 데이터 요청을 하고, 응답은 조회가 완료된 금융회사 데이터부터 WebSocket을 통해 받습니다. 이 방식은 사용자가 신속하고 원활하게 실시간 데이터를 수신할 수 있게 해줍니다. 이 접근법은 네트워크 트래픽과 서버 부하를 줄이는 동시에 사용자에게 빠른 반응 속도를 제공하는 이점이 있습니다.

한 번의 갱신 API 호출 후, Socket.IO 를 통하여 여러개의 응답을 받습니다.
한 번의 갱신 API 호출 후, Socket.IO 를 통하여 여러개의 응답을 받습니다.
// 각 금융회사의 잔액 정보를 저장하는 객체
const bankBalanceInfo = {
금융회사A: null,
금융회사B: null,
금융회사C: null,
}

// 캐시된 총 잔액 정보를 먼저 가져오는 함수
const fetchCachedBankTotalBalance = async () => {
const cachedBankTotalBalance = await fetch('/api/bank/cached-total-balance');
return cachedBankTotalBalance; // 예: { 금융회사A: 100,000,000원, 금융회사B: 200,000,000원, ... }
}

// 전체 은행 잔액을 갱신 요청
const updateRequestBankTotalBalance = async () => {
await fetch('/api/update-request/bank/total-balance');
}

// 금융사별 잔액 업데이트 및 화면에 표시
const updateAndRender = (bankCode, balance) => {
bankBalanceInfo[bankCode] = balance;
render(sum(bankBalanceInfo));
}

/* ================================================================== */

// WebSocket을 사용하여 갱신된 은행 잔액을 실시간으로 받을 준비
const socket = io();
socket.on("updateComplete", (bankCode, balance) => {
updateAndRender(bankCode, balance);
})

// 먼저 캐시된 총 잔액 정보를 화면에 표시
const cachedBankTotalBalance = await fetchCachedBankTotalBalance();
render(sum(cachedBankTotalBalance));

// 전체 은행 잔액을 갱신 요청
updateRequestBankTotalBalance();

이 방법을 통해 사용자는 더욱 신속하고 안정적으로 은행 잔액 정보를 확인할 수 있게 되었습니다!

“내자산” 에서 WebSocket을 단방향 통신에만 사용하여, 응답 결과를 수신하는 데에 한정하고 있습니다. 이는 WebSocket의 양방향 통신 능력을 완전히 활용하지 못하고 있음을 나타냅니다. 실제로, 단방향 통신 상황에서는 Server-Sent Events(SSE)를 도입하는 것이 더 적합한 대안이 될 수 있습니다.
또한, WebSocket을 통한 데이터 통신의 효율성을 높이긴 했지만, 일부 데이터는 여전히 별도의 HTTP 요청을 통해 수집되는 상황이 발생하고 있습니다. 이는 향후 개선이 필요한 영역입니다.

단순한 은행 총 잔액 컴포넌트
단순하지 않은 은행 총 잔액 컴포넌트

지금까지 은행 총 잔액 컴포넌트 구현을 위해 기본 API 요청부터 WebSocket 통신에 이르기까지 단계별 과정을 살펴보았습니다. 단순한 기술적 최적화를 넘어서, 사용자 경험의 지속적인 향상에도 주의 깊게 노력하고 있습니다.

이러한 점진적 개선 방식과 사용자 중심의 접근이 데이터 처리와 최적화를 고민하는 다른 개발자분들에게 조금이나마 도움이 되었기를 바랍니다.

이 글을 읽어주셔서 감사합니다! 네이버페이 내자산&회원FE 팀은 긍정적인 에너지와 열정을 가진 팀원들이 모여, 지속적으로 성장하고 발전하는 환경을 조성하고 있습니다.
이런 동기부여되는 분위기에서 함께 일할 새로운 팀원을 항상 찾고 있습니다. 저희 팀과 함께 성장하고 싶으신 분들은 언제나 환영합니다!

채용: https://recruit.naverfincorp.com/

--

--