기술 이야기
home
Programming
home

냐하! 우당탕탕 NIA 개인화 서비스 개발기

Upload date
2022/10/04
Tag
UX
NIA
Editor
마케팅컨텐츠개발팀_최종근, 연소라
Editor is
최종근 안녕하세요 마케팅컨텐츠개발팀 최종근입니다. 프론트엔드 개발하고 있습니다. UX를 중요하게 봅니다! 또한 좋은 사내 개발 문화를 위해 노력하고 있습니다. 연소라 안녕하세요 마케팅컨텐츠개발팀 연소라입니다. 프론트엔드 개발 업무를 진행하고 있습니다. 유저들이 좋아하고 즐길 수 있는 서비스를 개발하는 것이 목표이고, 유저들에게 더 좋은 UX 경험을 주고 싶습니다.
2 more properties

Table of contents

들어가며

안녕하세요 마케팅컨텐츠개발팀에서 웹 애플리케이션 엔지니어로 일하고 있는 최종근, 연소라입니다. 저희 팀에서는 넥슨 게임을 플레이하면서 생기는 수많은 데이터를 조합하여 유저에게 가치 있는 개인화 컨텐츠를 전달해 주는 일을 합니다. 이 글에서는 NIA 서비스, 그중에서 웹 어플리케이션을 개발하면서 크고 작은 이슈를 어떻게 기술적으로 풀어나갔는지 공유해 드리겠습니다.

NIA 란?

그림 1: NIA 4컷 만화 - NIA, 첫 출근하다! (넥슨플레이앱 NIA 공식친구를 통해 더 많은 만화를 보실 수 있습니다!)
NIA(니아)는 Nexon Innovative AI의 줄임말로 넥슨 인텔리전스랩스에서 제공하고 있는 인공지능(AI) 기술을 활용한 개인화 솔루션을 의미하며, 또한 신입 분석가라는 설정을 가진 공식 캐릭터의 이름으로도 쓰이고 있습니다. NIA는 시크하고 무뚝뚝한 성격이지만 츤데레같은 면모를 가졌는데요, 무심한 듯 툭 던지듯이 트렌디한 정보를 전달해 주는 친구입니다. (넥슨플레이 공식친구에서 ‘NIA’로 검색하면 친구 추가 할 수 있어요!)
더 구체적인 이야기가 궁금하다면, NIA의 자기소개와 기사를 참고해 주세요!
NIA 자기소개

NIA와 함께한 프로젝트

그림 3: NIA 개인화 명함
그림 4: NIA 개인화 메이플스토리 플레이로그 캘린더
NIA는 유저의 넥슨 게임 플레이로그를 바탕으로 다양한 맞춤형 서비스를 제공하기 위한 다양한 프로젝트들을 진행하고 있습니다. 그 중, NIA 개인화 명함 서비스 NIA 플레이로그 캘린더 서비스를 개발하는 과정에서 어떠한 일들이 있었는지 공유해보고자 합니다.
그림 5: NIA 개인화 명함 개발 초기 모습
NIA 개인화 명함 서비스는 자신만의 맞춤형 게임 명함을 만들어 볼 수 있는 서비스이며, NIA 플레이로그 캘린더 서비스는 유저 정보 및 플레이 로그와 게임 소식을 바탕으로 다양한 목적(바탕화면 설정, SNS 공유, 개인화된 정보 확인)으로 활용될 수 있는 개인화 캘린더를 제공하는 서비스입니다. 다소 라이트해 보이는 프로젝트이지만, 개발 과정에서 많은 문제에 직면했었는데요. 문제 사례와 그에 대한 해결 방법을 웹 개발을 중심으로 공유해 드리고자 합니다.

당면한 문제

문제 1. UX와 접근성 이슈

NIA 프로젝트들은 넥슨 회원이라면 누구나 스마트폰이나 컴퓨터로 쉽게 접근할 수 있도록 기획되었습니다. 유저들이 복잡한 조작 없이 누구나 만족할 만한 결과를 얻을 수 있도록 하는 것이 중요했습니다.

1.1 명함 연출 UX

NIA 개인화 명함 서비스에서는 유저들이 자신만의 명함을 생성하는 과정에서 기대감을 가질 수 있도록 화면을 연출하고자 하였습니다. 그래서 처음 명함을 생성할 때는 NIA가 열심히 명함을 만드는 알림 메시지가 뜨고 몇 초 후 지갑에서 명함이 미끄러져 올라오는 애니메이션을 의도하였습니다.
그림 6: 의도했던 UX (지갑이 빼꼼 보이면서, 명함은 완전히 보임)
하지만 개인화 명함 개발 중에 아래와 같은 버그를 리포팅 받았습니다.
그림 7 : 실제 초기 UX (맥북 에어 웹 해상도에서 닉네임 잘림 현상)
개발이나 실기기 테스트 단계에서 활용한 디바이스가 제한적이었기 때문에, 다양한 디바이스 사이즈와 비율을 고려하지 못해 좋지 못한 UX가 되었습니다.
본인의 명함인데 닉네임이 가려서 안 보이다니 있을 수 없는 일이죠!
위와 같은 상황을 방지하기 위해 디자이너와 여러 번 회의 끝에 극적으로 결과를 합의할 수 있었습니다.
NIA 개인화 명함에서 포기할 수 없는 UX는 다음과 같았습니다.
어느 디바이스에서건, 화면 하단의 콘텐츠로 유도하게끔 시각적인 힌트를 주는 스크롤 유도가 필요하다.
따라서, 지갑이 빼꼼 보이는 모습은 보장되어야 한다. (링크의 7번 참고)
지갑에서 명함이 튀어나올 때 가급적 모든 정보가 보여야 한다.
디바이스의 최소 가로 사이즈 320 px 에서 잘 작동되어야 함이 보장되어야 한다. (참고)
회전, 리사이즈 등으로 뷰포트 사이즈가 변하더라도, 자연스러운 명함 연출을 보여주어야 한다.
위와 같은 UX를 보장하기 위해 노력한 끝에 다음 영상과 같이 애니메이션 효과를 추가로 넣으면서, 명함을 자연스럽게 보여줄 수 있도록 연출하였습니다.
그림 8: 화면이 작을 때, 클 때, 리사이징 할 때, 추가 정보를 보고자 스크롤 할 때 대응 사례

1.2 의도하지 않은 자동 로그아웃

핵심 컨텐츠인 명함을 성공적으로 보여줬으니 이제는 서비스해도 문제없겠다고 생각했었는데, 팀원들과 QA를 진행하면서 예상치 못한 문제점을 발견했습니다.
공유받은 명함을 보려고 NIA에 접속하면 계속 로그아웃되었다는 창(alert)이 뜹니다.
이는 넥슨닷컴의 로그인 SDK를 적용하면서 발생했던 문제로, 넥슨닷컴은 보안 요구사항 때문에 SDK에서 세션 유지 시간을 짧게 유지하고 있습니다.
이로 인해 제한된 로그인 세션 시간에서 공유받은 명함을 보던 중, 로그인 세션이 종료되고 로그아웃 창이 출력되어, 유저 흐름이 끊기는 현상이 발생했습니다.
공유 받은 명함을 보는 페이지에서는 로그인 SDK의 기능을 잠시 비활성화 시키도록 조치했습니다. 실제 구현된 코드 형태는 약간 다르지만, 대략 다음과 비슷한 형태로 구현했습니다.
// 로그아웃 되었을 때 SDK에서 호출하는 Main Thread를 blocking 하는 alert 표시 방지 const originalAlert = window.alert window.alert = () => {/* no-op */};
TypeScript
복사
SDK에서 호출된 로그아웃 알림 alert 메시지를 오버라이딩해서 저희가 의도한 사용자 경험을 유지할 수 있도록 하였습니다.

1.3 Web & APP UX

NIA 개인화 명함 서비스가 웹이지만 저희는 사용하는 유저가 ‘앱’처럼 느끼도록 하였습니다. 빠른 패치와 대응이 가능하고 여러 플랫폼에서 실행할 수 있게 만들어 주는 웹의 장점을 살리면서도 스마트폰 사용과 일체화된 느낌을 주고 싶었습니다. 따라서 다음과 같은 작업을 추가로 하였습니다.
그림 9: 페이지 전환 시 크로스페이드 효과, 팝업 창 효과, 브라우저 히스토리 지원
페이지 전환 시 앱의 페이지 전환과 같은 transition 효과 부여
명함 수정 시, 수정 상세 페이지를 팝업 창이 덮는 효과 부여
명함 수정, 친구 명함 만들기 질문 등 뒤로 가기 및 앞으로 가기 브라우저 히스토리 조작 지원
페이지 전환 시 발생하는 크로스페이드 효과는 react-transition-group 의 도움을 받아 구현하였습니다. 두 컴포넌트가 교차할 때, 선언형으로 애니메이션을 정의할 수 있는 편리한 라이브러리였습니다.
팝업 창의 생성과 소멸은 react-domcreatePortal() API 의 도움을 받아 구현할 수 있었습니다. 팝업 컨텐츠(명함 수정 상세페이지)는 원본 컨텐츠 윗부분을 모달처럼 덮는 형태인데, 의도치 않게 포커스(초점)이 모달 뒤쪽에 있으면 버그 발생의 소지가 되므로, focus-trap-react의 도움을 받아 올바른 대상만 포커싱 될 수 있도록 하였습니다.
스타일링은 처음에는 sass를 사용하여 작업했으나, 반응형 작업에 대응하면서, CSS 미디어 쿼리와 레이아웃 작업을 좀 더 편하도록 하기 위해 tailwindcss로 전환하였습니다. 인라인 스타일을 작성하듯이 여러 미디어 쿼리를 대응할 수 있어서 생산성이 향상되었습니다.
개발 초기에는 반응형 레이아웃을 감지할 때 react-responsive를 사용하였으나, 개발 환경에서 반응형 대응이 제대로 되지 않아서 이슈를 발행하였고, 버그를 찾아 직접 수정하여 오픈소스에 기여하기도 했습니다.
그림 10: 버그를 수정하여 오픈소스에 기여한 화면

1.4 접근성

또한, 사용자의 접근성을 보장하기 위해 다음 원칙을 고수하였습니다.
접근성을 보장하고자 버튼과 에셋, (특히) 명함 부분에 자세한 설명을 달아 놓았습니다.
그림 11: 배경 이미지에 설명을 적어 놓은 모습 (이미지가 미처 다운로드 되지 못했을 경우에도 설명이 보이게 됨)
그림 12: 개인화 명함에 묘사할 수 있는 모든 문장을 설명
그림 13: 개인화 배너에도 상세한 설명을 달아 놓음
그림 14 : 키보드 Tab과 스페이스, 엔터만으로 모든 기능이 동작하도록 작업
그림 15: Lighthouse로 테스트한 접근성 점수 결과 100점

문제 2. 성능 이슈

2.1 웹 컨텐츠 전달

NIA 개인화 프로젝트는 유저의 이목을 끌기 위해 수많은 고화질 캐릭터, 이미지를 사용합니다. 그런데 대부분의 유저들은 상대적으로 성능과 인터넷 연결이 불안정한 모바일로 접속합니다. 실제로 타겟 유저 군도 넥슨플레이 앱을 통해 접속하도록 의도하였습니다.
유저가 몰리거나, 클라이언트나 프로토콜 상의 한계로 인해 컨텐츠 전달이 느려진다면, 결코 좋은 사용자 경험은 아닐 것입니다.
NIA 개인화 프로젝트에서는, 유저가 몰릴 것을 대비해 트래픽에 따라 자동으로 서버 수용량을 조절할 수 있는 AWS 인프라를 활용하여 많은 유저를 수용할 수 있도록 하였습니다. 또한, 에셋의 빠른 전달을 위해 분산된 CDN을 활용하여 정적 에셋과 일부 API를 제공할 수 있도록 구성하였습니다.
< AS-IS >
HTTP/1.1 프로토콜 사용
Frontend / API Server 종합 구성
< TO-BE >
HTTP/2 프로토콜 사용
Frontend 및 정적 Asset 빌드 → S3 웹 호스팅 + Cloudfront 정적 자원 캐싱 활용
Cloudfront 원본 라우팅 기능으로 동적 OpenGraph 기능 제공 (후술)

2.2 동적 이미지 컨텐츠 전달

NIA 개인화 프로젝트의 핵심 기능 중 하나는 유저의 게임 플레이 로그를 기반으로 개인화 이미지를 만들어 주는 작업입니다. 생성된 개인화 이미지는 사용자가 쌓은 데이터인 플레이 로그를 기반으로 생성되고, 수시로 변경됩니다. 그래서 계속해서 업데이트할 필요성이 있습니다.
NIA 메이플스토리 플레이로그 캘린더 서비스는 유저가 서비스에 접근할 때마다, 메이플스토리 대표 캐릭터의 코디, 레벨, 랭킹 등의 정보를 실시간으로 조회하여 캘린더 이미지를 생성합니다.
그림 16: 개인화 이미지 전달 플로우
위와 같은 흐름으로 개인화 이미지를 전달하는데, 사내 테스트 규모로도 사용자가 서비스에 접근할 때마다 이미지 생성 서비스에 부하가 가중되는 점을 확인했습니다. 수많은 사용자가 서비스를 이용하면서 병목현상이 발생하여 엔드 유저에게 좋지 않은 경험을 주게 될 것으로 예상했습니다.
그래서, 서비스 가용성 테스트를 위해 JMeter를 이용하여 Stress Test를 진행하여 서버 수용량을 점검하였습니다.
JMeter는 Java 기반의 성능 테스트 도구입니다. 성능 테스트를 통하여 응답시간(Response Time), 처리량(Throughput), 병목 구간 등을 확인할 수 있고, 시스템의 문제점을 파악하여 개선할 수 있습니다.
그림 17: JMeter(성능 테스트 도구) 실행 화면
JMeter를 실행하면 위와 같은 화면을 볼 수 있습니다. 서버 Request를 설정하고, Thread Properties를 수정하면서 서버에 부하를 줄 수 있습니다. 각 항목은 다음을 의미합니다.
Number of Threads (users): 동시에 몇 개의 스레드를 실행할지
Ramp-Up Period (seconds): 스레드를 Ramp-Up Period 시간 동안 실행. 예를 들어, Number of Thread가 10인데, Ramp-Up Period가 60이면, 스레드가 약 6초 간격으로 동작합니다.
Loop Count: 스레드의 반복 횟수
JMeter를 이용하여, Ramp-Up Period, Loop Count 값은 고정하고 Number of Threads를 조정하여, 서비스 병목의 원인을 추적하였습니다. 서비스 병목의 원인은 크게 두 가지로 파악되었습니다.
기존에 구현된 error handler가 적용되지 않았던 점
DB 부하 발생 시 connection이 끊기고 reconnection이 수행되지 않았던 점
위 내용을 개선하여, 1초당 100건의 요청을 안정적으로 성공시켰고, 평균 response time도 0.345s로 서비스 성능을 향상할 수 있었습니다.

문제 3. DX (Development eXperience) 및 서비스 확장 이슈

NIA 개인화 프로젝트는 서버와 클라이언트가 밀접하게 연관되어 있습니다. 따라서 개발을 같이 해야 하는 경우가 잦은데, 멀티 레포지토리로 되어 있어 의존하는 부분을 찾기 어렵거나 참조가 어렵거나 비슷한 코드(타입 등)를 중복으로 작성해야 하는 이슈가 있었습니다.
처음 작업할 때는 git submodule을 활용하여 서버 밑으로 클라이언트를 두는 방식으로 작업했습니다. 처음에는 서버와 클라이언트 1:1 구조로 작업하였기 때문에 비교적 불편함 없이 개발할 수 있었습니다. 하지만 서비스가 확장되면서 점차 불편사항들이 늘어나기 시작했습니다.
비슷한 컴포넌트의 중복 개발
여러 레포지토리 관리로 인한 인프라 비용 상승
타입 정의 중복
의존성 버전 차이로 인한 잠재적 버그
사이드 이펙트 발생 우려
따라서 레포지토리를 단일화하면서 여러 프로젝트(하지만 비슷한)를 동시에 관리하기로 하였고, 다음과 같은 프로젝트 구조를 설계하고 잡았습니다.
yarn workspace를 활용한 의존성 및 프로젝트 관리
다음 구조로 워크스페이스를 격리하였고, 필요한 경우 외부 의존성처럼 사용할 수 있도록 구성하였습니다. 의존성이 꼬이지 않도록 세심하게 번들러 설정도 했습니다.
node_modules # packages 에 있는 모든 workspace의 의존성 관리 packages - server # NIA 프로젝트 관련 API 서버 - calendar # 개인화 플레이로그 메이플스토리 캘린더 프론트엔드 - card # 개인화 명함 프론트엔드 - common # 서버 / 클라이언트 공용 라이브러리 - types # 서버 / 클라이언트 공용 타입 정의 - ui # 공용 프론트엔드 컴포넌트 package.json
Markdown
복사
packages에 있는 여러 workspace 들은 각각의 package.json을 가지고 있으며, node_modules폴더도 소유합니다. 단 node_modules폴더 내에는 루트의 node_modules를 가리키는 심볼릭 링크만이 존재하여 의존성을 직접 설치하진 않습니다.
다음 소스 코드는 calendar 워크스페이스에서 다른 워크스페이스에 있는 모듈을 사용하는 예제입니다.
import { useState } from "react"; import SharedButton from "ui/SharedButton"; import { Session } from "types/session"; function Landing() { const [session, setSession] = useState<Session>({ isLogin: false, nexonSN: 0, }); // ... return ( <div> <button className="p-4 bg-slate-100" onClick={() => setSession({ isLogin: true, nexonSN: 123456789 })} > Login </button> <SharedButton color="yellow">내 캘린더 보러 가기</SharedButton> </div> ); }
TypeScript
복사
yarn workspace의 도움을 받아 모노레포 구조로 작업하니 도메인 단위로 개발을 진행할 수 있게 되었습니다. 서로의 자원 (모듈)도 명시적으로 가져오고 내보내면서 작업할 수 있었던 덕분에, 이 방식대로 작업하면 서비스 확장에도 크게 복잡해지지 않는 서비스 구조가 되어 생산성 향상에 도움을 주었습니다.
YAGNI 원칙에 따라, LernaTurborepo 등을 사용하지 않고 간단하게 구성했으며 추후 서비스 확장 시 필요하다고 판단되면 적절한 프레임워크를 도입할 예정입니다.

문제 4. 개인화 Opengraph 이슈

NIA 개인화 서비스의 개인화 컨텐츠 결과물은 다른 유저에게 공유할 수 있고, SNS에도 게시될 수 있습니다. 보통은 URL 미리보기를 제공해 주어 어떤 URL인지 알아낼 수 있는 메커니즘이 있는데, 보통은 OpenGraph를 사용합니다. <meta> 태그에 URL에 대한 정보가 있다면 검색엔진이나 크롤러가 이를 해석하여 미리보기를 생성하게 됩니다.
예시)
그림 18: 카카오톡 링크 공유 시, URL 미리보기가 생성된 화면
하지만, 프론트엔드를 정적 에셋 빌드로 구축해 놓은 상황에서, 모든 것을 클라이언트 렌더링에 의존하는 구조에서는 개인화된 컨텐츠의 Opengraph 생성은 일반적인 방법으로는 어려웠습니다.
그림 19: NIA 개인화 서비스 접근 Flow
그래서 저희는 이미 인프라로 활용하고 있는 Cloudfront의 ‘원본’과 ‘동작’ 설정을 이용하여 해결하고자 했습니다.
그림 20: 첫 페이지 요청 시 EC2서버를 거치도록 개선된 NIA 개인화 서비스 접근 플로우
엔드포인트 요청이 개인화 컨텐츠 면 개인화된 URL 메타정보를 추가로 얻어올 수 있도록 하였습니다. 그 외의 페이지에서는 그대로 web root (index.html) 파일을 전달해주기 위해 위와 같은 컨텐츠 요청 흐름을 설계하였습니다. EC2 서버에서는 User Agent를 분석하여 일반 사용자가 브라우저로 접속하였다면 평소대로 index.html 컨텐츠를 돌려주고, 봇이라면 추가적인 연산을 통해 개인화 컨텐츠를 가져와 <meta> 태그에 부착하여 돌려주게 됩니다.
결과적으로는 잘 동작하여 효과적으로 유저에게 미리보기 컨텐츠를 전달하여 적은 비용으로 원하는 목적을 달성할 수 있었습니다.

나가며

세심하게 점검하고 다양한 테스트를 거쳤기 때문에, 귀여운 NIA 모습이 더욱더 인상적으로 다가오는 뿌듯했던 첫 프로젝트였습니다. 프로젝트 제작에 참여해 주신 분들에게 이 자리를 빌어 감사하다는 인사를 하겠습니다.

Reference

펼쳐보기
Techblog Contents
Related Sites
 넥슨 게임 포탈
회사 소개
인텔리전스랩스 소개
인재 영입
인텔리전스랩스 블로그 운영 정책
 테크블로그 문의 devrel@nexon.co.kr