기술 블로그 모음

국내 IT 기업들의 기술 블로그 글을 한 곳에서 모아보세요

전체 프론트엔드 백엔드 데브옵스 AI 아키텍처 DB 네트워크 보안 기타
DX본부 홈쇼핑검색추천파트 추천서비스개발 담당 경력사원 채용(~10/27)
GS리테일
DX본부 홈쇼핑검색추천파트 추천서비스개발 담당 경력사원 채용(~10/27)

올리브영 물류시스템에서는 데이터를 어떻게 주고 받을까?
올리브영
올리브영 물류시스템에서는 데이터를 어떻게 주고 받을까?

Kafka 메시지 중복 및 유실 케이스별 해결 방법
올리브영
Kafka 메시지 중복 및 유실 케이스별 해결 방법

안녕하세요. 풀필먼트 스쿼드에서 백엔드 개발을 담당하고 있는 시나브로우입니다. 저는 최근 공급망 관리(Supply Chain Management)를 통해 올리브영의 비즈니스 목표를 극대화하는 SCM 스쿼드에서 이적하였는데요. 이적 전인 2024년…

Business AI Agent로 혁신하는 업무 프로세스
삼성 SDS
Business AI Agent로 혁신하는 업무 프로세스

이 아티클의 핵심주제는 AI 에이전트로, 이는 사용자들의 업무 특성을 파악하여 맞춤형으로 서비스를 제공하며, 기업의 전반적인 효율성과 경쟁력을 끌어올릴 것으로 전망합니다.

하이버네이트의 시간은 거꾸로 간다
마켓컬리
하이버네이트의 시간은 거꾸로 간다

스프링부트 버전을 업그레이드하는 과정에서 발견된 버그 해결기

생성형 AI 시대, 데이터 사이언티스트는 무슨 일을 할까?
삼성 SDS
생성형 AI 시대, 데이터 사이언티스트는 무슨 일을 할까?

생성형 AI 시대에서 성공하려면 데이터 사이언티스트는 그들의 접근 방식을 재고해야 합니다. 포괄적이고 효율적인 작업을 수행하는 방법을 배우기 위해 비정형 데이터 소스를 통합하고, AI 윤리를 실행하고, 새로운 도구를 활용하는 등 필수 영역에 초점을 맞추어야 합니다.

Hyperautomation 추진을 위한 고려 사항 5가지
삼성 SDS
Hyperautomation 추진을 위한 고려 사항 5가지

이번 아티클은 2024년 9월, 삼성SDS가 대외 고객을 대상으로 진행한 「REAL SUMMIT 2024」 행사 중, ‘Hyperautomation 추진을 위한 고려 사항 5가지’ 세션 발표 내용을 기반으로 작성되었습니다.

FAST: 데이터 파이프라인 이제는 웹에서
네이버 페이
FAST: 데이터 파이프라인 이제는 웹에서

목차0. 들어가며 0.1 데이터 활용을 위해 필수불가결한 배치 시스템1. 기존 데이터 활용 배치 시스템의 문제점 1.1 Zeppelin Cron 기능을 이용한 배치 1.2 Airflow기반 파이프라인 생성 배치 1.3 Redash를 통해 주기적으로 대시보드생성2. 새로운 데이터 활용 배치 시스템 2.1 요구사항 및 설계 2.2 아키텍처 3. FAST의 주요기능 3.1 배치 조회 3.2 배치 생성 3.3 이외의 기능4. 도입 후: 기존 프로세스 vs FAST 활용 프로세스 4.1 AS-IS: 기존 프로세스 4.2 TO-BE: FAST 활용 프로세스5. 맺으며안녕하세요, 네이버페이 인텔리전스플랫폼팀 신태범입니다.0. 들어가며저희 팀은 데이터 엔지니어링 팀으로, 조직 내에서 데이터가 성공적으로 활용될 수 있도록 여러 업무를 수행하고 있습니다. 이를 구체적으로 살펴보면, 데이터 인프라 운영과 관리, 네이버페이 서비스에서 생성되는 데이터의 수집, 그리고 데이터 거버넌스를 포함합니다.팀에서 담당중인 업무들데이터 문화가 고도화됨에 따라, 데이터팀이 해야 할 역할은 더욱 다양해지고 있습니다. 그 중 하나는 서비스 담당자들이 최소한의 개발 지식으로도 도메인 지식을 활용해 데이터를 쉽게 다룰 수 있도록 지원하는 것입니다.개인이나 조직이 데이터를 효과적으로 이해하고 활용할 수 있도록 하는 시스템을 ‘데이터 리터러시 플랫폼’이라고 부릅니다. 사용자들이 필요로 하는 데이터 리터러시 도구들을 지속적으로 개선하고 개발하는 것은 조직의 데이터 리터러시를 높이는 중요한 방법 중 하나입니다.이 글에서는 저희 팀의 데이터 리터러시 플랫폼 중 웹기반 데이터 파이프라인 생성 툴인 FAST에 대해 소개하고자 합니다. FAST는 FDC automated self tasker의 약자로 빠르게 배치를 구성할 수 있다는 의미를 담고있습니다. 기능적으로는 웹기반으로 사용자 입력을 받아 Airflow DAG 생성을 자동으로 연계해주는 툴입니다.이 글이 정형화된 파이프라인 템플릿을 보유하고 있거나, 파이프라인을 전사적으로 효과적으로 활용할 방법을 찾고 계신 분들께 도움이 되길 바랍니다. 또한, 파이프라인이 없더라도 조직 내 데이터 리터러시를 향상시키기 위해 고민하는 분들께 유용한 참고자료가 되었으면 좋겠습니다.0.1 데이터 활용을 위해 필수불가결한 배치 시스템### S님의 업무일지처음에는 필요한 데이터를 얻기 위해 제플린 같은 도구를 사용해 직접 찾아보는 단계에서 시작했습니다.하지만 시간이 흐르면서, 매일매일 같은 종류의 데이터를 볼 필요성이 생기기 시작했죠.그래서 우리 팀에선 그런 데이터를 쉽게 볼 수 있게 '마트 테이블'이라는 것을 만들기 시작했습니다. 그리고 일별, 주별, 월별로 데이터를 살펴보고 이상치가 없는지 확인하며, 중요한 부분들을 눈에 띄게 '시각화'하는 걸 배웠습니다.이건 Redash라는 것을 사용해서 가능했죠.그리고 이런 정보들이 필요하다고 생각되는 팀원들에게나, 전사적으로 중요한 KPI와 관련 있는 정보들은 함께 공유하기 시작했습니다.처음엔 데이터를 보기 위한 SQL 쿼리를 작성하는 것이 어려웠지만, 점차 익숙해지더군요.그리고 그 다음 단계로, 데이터를 뽑고 분석하는 과정을 '자동화'하는 방법을 배우게 되었습니다.이에 '배치 시스템'이라는 것을 활용하게 되었죠.이렇게 하면 데이터 처리 과정을 편하게 자동으로 돌릴 수 있어 시간을 절약하여 업무 효율을 높일 수 있었습니다.데이터 기반 의사결정 조직에서 사용자들은 어떻게 데이터 문화에 익숙해져갈까요? 처음에는 SQL을 직접 사용하거나 팀에서 공유받은 쿼리 템플릿을 활용하여 필요할 때마다 제플린 등의 데이터 분석 노트북 툴로 단순히 데이터를 조회합니다.일시적인 쿼리뿐 아니라, 의사결정을 위해 주기적으로 데이터를 추출해야 하는 경우도 있습니다. 이러한 작업이 반복되면 자주 사용하는 테이블을 JOIN하여 다른 구성원들이 쉽게 활용할 수 있도록 미리 마트 테이블을 생성하게 됩니다.사용자들은 이렇게 생성한 마트 테이블을 활용해 일별, 주별, 월별 등으로 이상치와 집계 결과를 정기적으로 확인합니다. 또한, BI 도구로 보다 직관적으로 결과를 시각화하고, 필요한 경우 팀원들과 공유하며 의사결정을 진행할 수 있습니다. 더 나아가 조직의 중요한 KPI와 관련된 경우 전사에 공유하는 자료에도 이를 활용할 수 있습니다.이 과정을 반복하게 되면 사용자들은 쿼리에 빠르게 익숙해지고 능숙하게 데이터를 볼 수 있게 됩니다. 사용자는 계속해서 반복되는 데이터 추출 관련 업무에 쏟는 시간을 최소화하고 효율적으로 일하기 위해 자동화 배치 시스템을 사용하고자합니다.실제 데이터 활용 시나리오1. 기존 데이터 활용 배치 시스템의 문제점위에서 설명한 흐름에 따라 점차 구성원들이 데이터 보는 법을 알게 되고, 사내에 데이터 문화가 자리 잡아갑니다. 네이버 페이 역시 구성원들의 데이터 리터러시가 향상되며 배치 시스템의 니즈는 커졌습니다.이에 따라 저희 팀에서도 사용자들의 니즈에 맞춰 다양한 배치 시스템을 지원 했습니다. 크게 아래 네가지로 나누어집니다.1. Zeppelin상에서 작성한 집계쿼리의 결과를 주기적으로 메일로 받아보고 싶어요- Zeppelin Cron2. 여러 서비스 시스템 간의 수치가 맞는지 맞춰보고 싶어요(시스템 간 대사)- 데이터 엔지니어가 직접 Airflow DAG로 배치 생성3. 좀 더 안정적인 배치를 통해 사용할 마트 테이블을 생성하고 싶어요- 서비스 실무자가 코드작성, 데이터 엔지니어 리뷰를 거쳐 Airflow DAG로 배치 생성4. 데이터를 활용해서 시각화하여 대시보드를 생성하고 싶어요- Redash를 통해 주기적으로 결과가 갱신되는 대시보드 작성그러나 시스템 도입 초기부터 앞으로의 모든 문제를 예측해 설계하는 것은 현실적으로 불가능합니다. 기존 프로세스 또한 당시의 사용자 니즈에 맞춰 도입되다 보니 시간이 지나며 다음과 같은 문제가 발생했습니다.1.1 Zeppelin Cron 기능을 이용한 배치Zeppelin CronZeppelin Cron은 노트북에 스케줄을 설정하여, 설정된 스케줄에 맞춰 노트북이 자동으로 실행되도록 하는 기능입니다. 제공되는 템플릿에 사용자가 결과를 보고 싶은 쿼리만 넣어주고, 스케줄만 지정하면 간단히 사용가능하다는 장점이 있었는데요. 하지만 이 방식에는 아래와 같은 문제점이 있었습니다.1.2 Airflow기반 파이프라인 생성 배치Airflow기반 파이프라인 생성 배치일부 데이터는 서비스와 밀접한 관련이 있기에, 안정적인 배치를 위해 내부 파이프라인 라이브러리를 통해 데이터를 추출 하기도 했습니다. 하지만 YAML, git, jenkins, github 등 비개발자에게는 낯선 지식이 필요하다는 허들이 존재했고, 수정이 필요할 때마다 데이터 엔지니어와의 커뮤니케이션이 필요하다는 문제점이 있었습니다.1.3 Redash를 통해 주기적으로 대시보드생성Redash 대시보드 화면숫자나 문자로 표현된 정보는 직관적이지 않습니다. 데이터를 한 눈에 시각화하고 인사이트를 효과적으로 전달할 수 있도록 Redash를 도입했습니다. 대시보드 내부의 데이터 배치는 Redash Scheduled Query 기능을 통해 주기적으로 갱신합니다.하지만 매번 필요할 때마다 Redash에 접속해야 한다는 불편함이 있어 주기적으로 대시보드를 메일로 받을 수 있는 기능에 대한 갈증이 존재했습니다. 또한 대시보드를 생성하기 위한 마트 테이블 생성을 위해 과도하게 무거운 쿼리가 실행되는 경우가 잦아 리소스 상의 문제가 발생하여 정상적으로 배치가 수행되지 않기도 했습니다.이외 문제점위에서 언급한 컴포넌트별 문제뿐만 아니라, 산발적으로 여러 컴포넌트에서 다양한 배치가 수행되고 있다보니 팀에서 관리할 포인트가 느는 등 유지보수 상에서도 불필요하게 공수를 잡아 먹는 문제가 있었습니다.2. 새로운 데이터 활용 배치 시스템위와 같은 사용자/관리자 입장에서의 불편함으로 인해서, 기존 데이터 활용 프로세스를 개선한 새로운 데이터 활용 배치 시스템이 필요했습니다.저희의 주된 사용자 분들 중에는 개발관련 지식에 친숙하지 않은 서비스 실무자 분들이 포함되어 있습니다. 그렇기에 누구나 쉽게 사용할 수 있도록, 웹기반으로 좀 더 직관적이고 접근성이 높은 툴을 개발하자는 결론을 내렸습니다.추가적으로, 개별 컴포넌트들의 문제 및 운영/관리 상의 문제와 추후 조직의 KPI를 취합하여 정리했을 때 아래 요구사항을 충족하는 툴을 만들고자 했습니다.2.1 요구사항 및 설계요구사항2.2 아키텍처FAST는 위에서 언급한 요구사항을 고려해서 다음과 같이 설계되었습니다FAST 아키텍처실제 배치는 안전성과 유지보수를 고려하여 workflow 툴인 Airflow를 사용Hive(JDBC), Bash, TextMailing, ScreenshotMailing, Join Task(Operator) 등을 쉽게 정의할 수 있게 웹에서 제공사용자는 필요한 Task를 추가하고, 각 Task의 필수 값을 입력만 하는 형태로 간단하게 안정적인 배치를 구성할 수 있음웹에서 구성한 데이터 파이프라인을 Python 코드와 YAML 구성 파일로 자동 변환한 후, 이를 Airflow로 배포하여 Airflow DAG으로 변환3. FAST의 주요기능앞서 소개드린 요구사항과 설계에 맞춰 새로운 데이터 활용 배치시스템을 개발하였습니다. 이 시스템은 빠르게 배치를 구성할 수 있다는 의미를 담아, FDC automated self tasker, FAST로 명명했습니다.웹 UI에서 정해진 필드를 단순히 채우기만 하면 배치를 생성할 수 있도록 개발하여, 그간의 데이터 파이프라인을 구성하기 위한 허들을 많이 낮추었고, 이를 통해 사용성은 높였습니다. FAST의 UI와 주요기능은 아래와 같습니다.3.1 배치 조회FAST Home3.2 배치 생성배치 생성 UI는 두개의 화면으로 나누어져 있습니다. 오른쪽에는 각 Task에서 필요한 인풋들을 입력받는 폼이 있습니다. 왼쪽에는 오른쪽에서 각 Task별로 설정한 디펜던시를 그래프를 보여줘 보다 직관적으로 실행흐름을 확인할 수 있도록 하고 있습니다.Task 설명ScreenshotMailing: 대시보드를 메일링하는 TaskTextMailing: 쿼리 결과를 메일링하는 TaskHive: hive 쿼리를 실행하는 TaskTask별 쿼리에 대해 쿼리검증 버튼을 누르면 실행전 오류를 미리 탐지3.3 이외의 기능이외에도 사용자 편의를 생각한 여러 기능을 제공 중입니다. 자주 사용되는 패턴은 템플릿으로 제공하며, 필요에 따라 이 템플릿을 복제해서 값만 대치하는 형태로 활용할 수 있도록 하고 있습니다. 또한 FAST는 git 커밋로그처럼 변경 메시지와 함께 변경 내역을 관리하고 있어서 과거 버전을 조회하거나 롤백할 수도 있습니다.다른 유저가 작성한 배치 복사배포 이력 확인배포 롤백배치 복제팀계정(키탭) 지원이외에도 사용자 요구사항을 지속적으로 반영하기 위해 사내 데이터 문의창구를 별도로 운영 중입니다. 무엇보다도, 사용자 편의성을 최고로 우선시하여 의견을 적극 반영하여 시스템을 개선해 나가고 있습니다.4. 도입 후: 기존 프로세스 vs FAST 활용 프로세스마지막으로, FAST 도입 전과 도입 후 사용자 입장에서 어떻게 데이터 파이프라인 작성 과정이 변경되었는지 살펴보도록 하겠습니다.AS-IS -> TO-BE4.1 AS-IS: 기존 프로세스기존 프로세스에서는 마트 생성을 위한 배치를 만들기 위해 사용자가 SQL 문법 이외에도 여러 개발지식을 익힐 필요가 있었습니다.사용자가 필요한 지식: git, 환경세팅, YAML, SQL, ndeploy(사내 배포툴)step1 개발환경을 세팅한다(git, vscode 등 설치)step2 YAML, git에 대한 지식을 쌓는다.step3 기존에 작성되어있던 예시를 보고 코드를 작성한다.step4 배포해본다. -> 왜안되지?step5 데이터 엔지니어에게 물어본다.step6 다시 수정한다. -> 배포해본다. -> 왜안되지? ...step7 어찌저찌 완성.. 하고 데이터 엔지니어에게 배포를 요청한다.step8 모니터링은 데이터 엔지니어가 해줌step9 원하는 결과가 나올때까지 반복4.2 TO-BE: FAST 활용 프로세스사용자는 기존에 학습한 SQL 문법과 간단한 FAST 사용법만 인지하면 쉽게 마트 테이블 생성 배치를 만들 수 있게 되었습니다.사용자가 필요한 지식: SQL, FAST 사용법step1 기존에 작성되어 있는 예시를 보고 input값을 적는다.step2 저장하고 배포한다.step3 원하는 결과가 나올때까지 반복5. 맺으며지금까지 개발지식이 필요했던 여러 배치시스템을 웹기반으로 통합한 FAST에 대해 소개드렸습니다. FAST 도입 이후, 사용자는 훨씬 적은 기반지식을 가지고 쉽게 배치를 생성할 수 있게 되었습니다. 또한 저희 팀에서는 커뮤니케이션 및 산발적인 컴포넌트를 관리를 위한 공수가 줄어들어, 인프라 고도화 및 다른 업무에 집중할 수 있게 되었습니다.추후 개발FAST의 성공적인 안착 이후, 현재 비개발자 분들의 손쉬운 모델 개발을 위한 AutoML 및 사용성을 높이기 위한 python, Spark 실행을 지원하는 Pyspark 등을 도입하기위해 내년 상반기들 목표로 인텔리전스서비스팀과 협업하여, 개발중에 있습니다.이를 통해 FAST는 데이터 파이프라인을 넘어서, MLops까지 범주를 확장하여 All-in-one 데이터 리터러시 플랫폼이 되고자 합니다. 프로젝트에 도움을 주신 분들께 감사드리며, 긴 글 읽어주셔서 감사합니다.FAST: 데이터 파이프라인 이제는 웹에서 was originally published in NAVER Pay Dev Blog on Medium, where people are continuing the conversation by highlighting and responding to this story.

Tech Week 2024, 도쿄에 다녀왔습니다.
라인
Tech Week 2024, 도쿄에 다녀왔습니다.

안녕하세요. LINE VOOM의 추천 시스템을 개발하는 ML 엔지니어 이창현, 미디어 플랫폼 서버 개발자 조희성입니다. 저희는 지난 9월 2일부터 6일까지 개최된 사내 행사인 Te...

Behind the Paper: 하이퍼커넥트 AI 조직이 제품에 기여하면서 연구하는 법
하이퍼커넥트
Behind the Paper: 하이퍼커넥트 AI 조직이 제품에 기여하면서 연구하는 법

하이퍼커넥트는 오랜 기간 제품에 기여하는 AI 기술을 연구해 왔습니다. AI를 통해 제품에 기여한다고 하면 성과지표(KPI)를 설정하고 적당한 모델을 찾은 뒤 가능한 한 빨리 시장에 제품을 출시하는 것으로 여겨지기 쉽습니다. 반대로 연구라고 하면 논문 작성과 동일시되는 경향이 있고요. 하지만 하이퍼커넥트의 AI 조직이 일하는 방식은 이러한 일반적인 인...

생성형 AI 시대, 성공적인 디지털 트랜스포메이션을 위한 검증법
삼성 SDS
생성형 AI 시대, 성공적인 디지털 트랜스포메이션을 위한 검증법

이 글은 디지털 트랜스포메이션(DX)은 기술이 아닌 비즈니스 성장과 혁신에 초점을 맞춰야 하며, 전사적 협력과 현장 의견 반영이 중요함을 강조합니다.

NHN Cloud Plugin 개발기 | Cloudforet 오픈소스 프로젝트 후기
NHN 클라우드
NHN Cloud Plugin 개발기 | Cloudforet 오픈소스 프로젝트 후기

![foret_main.png](https://image.toast.com/aaaadh/real/2024/techblog/foretmain.png) >세계 최대 벤더 중립 오픈소스 커뮤니티 Linux Foundation 멘토십 프로그램으로 2024년 6월~8월 진행된 [Cloudforet NHN Cloud Plugin 개발 오픈소스 프로젝트] 멘티 학...

Uplift Modeling을 통한 마케팅 비용 최적화 (with Multiple Treatments)
네이버 페이
Uplift Modeling을 통한 마케팅 비용 최적화 (with Multiple Treatments)

안녕하세요, 네이버페이 인텔리전스서비스 박대한입니다. 마케팅 비용 최적화 문제를 uplift modeling으로 정의하고, 인과추론(causal inference) 모델을 적용한 사례를 공유드립니다. 이론이나 코드보다는 분석 과정에서의 실무적 고민과 생각을 중심으로 이야기하려 합니다. Uplift modeling으로, CATE, 다중 처치(Multiple Treatments)에 관심 있는 분들께 참고가 되길 바랍니다.글의 순서는 다음과 같습니다.0. 문제 정의 0.1. Uplift Modeling 0.2. Multiple Treatments1. 데이터 수집2. 모델 개발 2.1. Double Machine Learning 2.2. 인과효과 추정3. Offline 평가4. Policy 생성5. Online 평가6. 후기Source: Unsplash0. 문제 정의네이버페이는 서비스를 활성화하기 위해 자체적으로 프로모션을 진행합니다. 예를 들어, 마이데이터 등록, 자동차보험 등록, 제휴 가맹점 결제 등과 같은 전환 관련 action을 유도하기 위해 “OOO 하시면 네이버페이 포인트 1000원을 드립니다”와 같은 프로모션을 진행합니다.Npay 마이데이터 만기연장 프로모션여기서 ”어떤 사용자들에게 1000원을 제공해야 하는가?” 라는 마케팅 비용 최적화 문제가 발생합니다. 만약 마케팅 없이도 목표 action을 할 사용자가 있다면, 이들에게는 마케팅을 하지 않는 것이 비용 절감에 도움이 될 것입니다. 마케팅 분야에서는 이를 uplift modeling 문제라고 합니다.또한, ”1000원에 반응하지 않는 사용자들에게 2000원을 제공하면 반응할까?” 라는 질문도 할 수 있습니다. 사용자마다 마케팅에 반응하는 가치가 다를 수 있기 때문입니다. 저는 이 문제를 Multiple treatments 문제로 정의했습니다. 자세한 내용은 아래에서 설명드리겠습니다.0.1. Uplift ModelingCausal inference에서는 변수들을 크게 treatment, outcome, confounder로 구분합니다. treatment(T)는 원인, outcome(Y)은 결과, confounder(X)는 treatment와 outcome에 모두 영향을 미치는 변수를 의미합니다. Uplift modeling은 개별 또는 그룹 수준에서 treatment의 인과 효과를 추정하고 예측하는 기법입니다 [1][2]. 마케팅에서는 이러한 인과 효과를 uplift라고 부릅니다. 예를 들어, “30대 사용자 A가 평소에 10원을 결제하지만 마케팅 후 100원을 결제한다”라면, treatment는 마케팅 여부, outcome은 결제 금액, confounder는 30대, Uplift는 100–10 = 90이 됩니다. Causal inference 분야에서는 uplift보다 Conditional Average Treatment Effect(CATE)가 더 일반적으로 사용됩니다. 이제부터 인과 효과를 uplift 대신 CATE로 명명하겠습니다.Uplift modeling 문제를 treatment를 받았을경우 전환여부(y축)와 받지 않았을 경우(x축) 두 축으로 나누면 다음과 같이 분류할 수 있습니다.Source: https://ambiata.com/blog/2020-07-07-uplift-modeling/- (1) Sure things(항상 구매 고객): 마케팅과 관계없이, 항상 제품을 구매하는 고객 (CATE = 0)- (2) Persuadables(설득 가능 고객): 마케팅 후, 구매 가능성이 높아진 고객 (CATE > 0)- (3) Lost causes(항상 비구매 고객): 마케팅과 관계없이, 구매 의사가 없는 고객 (CATE = 0)- (4) Sleeping dogs(청개구리 고객): 마케팅 후, 구매 가능성이 낮아진 고객 (CATE < 0)마케팅에서 집중해야 할 그룹은 CATE가 0보다 큰 (2) Persuadables입니다. 다른 그룹들은 마케팅 효과가 없거나 부정적이지만, Persuadables 그룹에서는 긍정적인 효과가 발생합니다. 즉, 해결해야 할 문제는 ”어떤 사용자가 Persuadables인가?”, 즉 “CATE가 큰 사용자가 누구인가?”를 예측하는 것입니다.0.2. Multiple TreatmentsTreatment를 마케팅 금액이고 금액은 1000원, 2000원으로 가정해보겠습니다. 이를 연속형(continuous)으로 볼 수도 있지만, 유니크한 값이 세 개뿐이므로 연속형으로 보는 것은 큰 의미가 없다고 판단했습니다. 또한, 장기적으로treatment를 노출 채널, 발송 요일 등으로 확장할 수 있을 것이라 생각했습니다. 확장성을 고려하면 처음부터 범주형(categorical)으로 정의하는 것이 더 적합하다고 판단했습니다.지금까지 uplift modeling이 해결하고자 하는 문제와 multple treatments로 정의한 이유를 살펴봤습니다. 다음으로는 실제 작업 순서를 따라 1.데이터 수집, 2.모델 학습, 3.Offline 평가, 4.Policy 생성, 5.Online 평가를 설명하겠습니다.1. 데이터 수집모델 학습을 위해 랜덤화(randomization)된 interventional data를 수집합니다. 예를 들어, treatment가 0원, 1000원, 2000원이라면, 사용자에게 이 금액들을 랜덤으로 제공하고 결제 여부를 수집합니다. treatment를 랜덤으로 할당하는 이유는 다른 요인의 영향을 배제하기 위해서입니다. 이를 통해 observational data 분석에서 발생하는 교란(confounding) 문제를 사전에 제거할 수 있습니다. Confounding은 treatment와 outcome 모두에 영향을 미치는 공통 원인이 존재하므로써 발생하는 bias를 의미합니다. [3]예를 들어, “30대 사용자만을 대상으로 결제를 유도하는 마케팅”한 데이터를 사용한다고 가정하면, treatment는 마케팅 노출 여부, outcome은 결제 여부, confounder는 30대가 됩니다. 이 데이터를 그대로 사용해 모델을 만든다면, 30대라는 confounder가 treatment에 영향을 미치는 것을 고려해야 합니다. 순수 마케팅 효과인지 30대라는 특성 때문인지 구분하기 어렵기 때문입니다. 하지만 randomization을 통해 연령대를 포함한 다양한 사용자를 대상으로 하면, confounder가 제거된 상태에서 순수한 마케팅 효과를 측정할 수 있습니다. Observational data에 대한 더 자세한 내용은 강의 영상을 참고해주시기 바랍니다.Source: https://causalinference.gitlab.io/causal-reasoning-book-chapter1/#fig:rct-confounders또한, practical한 측면에서는 “어떤 금액이 가장 효과적인가?”를 미리 확인할 수 있다는 장점이 있습니다. AB test를 수행하는 것과 사실상 동일하기 때문입니다. 예를 들어, 2000원의 전환율이 1000원보다 높지 않다면, 앞으로 개발할 모델에서도 2000원을 배제하고 1000원만 고려하는 것이 ROI 측면에서 효율적일 것입니다. 물론, 이러한 실험은 예산 범위 내에서만 가능합니다.2. 모델 개발Causal inference 분야에서는 CATE를 추정하기 위한 다양한 모델이 있습니다. 아래 표는 Microsoft Research에서 개발한 EconML의 모델 리스트입니다. 저는 최종적으로 Double Machine Learning(DML)을 선택했습니다. DML이 무엇인지, 그리고 왜 선택했는지 설명드리겠습니다.Source: KDD 2021 Introduction to EconML2.1. Double Machine LearningDML은 비모수적(non-parmetric) ML 모델의 유연성을 활용하여 낮은 편향(bias)과 유효한 신뢰 구간을 제공하는 CATE 추정 알고리즘입니다. Debiased machine learning 또는 orthogonal machine learning으로도 알려져 있습니다. [3]DML의 첫 번째 특징은 orthogonalization입니다. 이는 outcome과 treatment에 영향을 미치는 confounder로부터 비인과적 효과를 분리하는 과정입니다. 이 과정에서 xgboost와 같이 비선형 관계를 학습하는 ML 모델을 사용할 수 있습니다. DML에서는 총 3개의 모델이 학습됩니다. Confounder(X)로부터 outcome(Y)를 예측하는 모델 M_y, treatment를 예측하는 모델 M_t, 그리고 residual Y와 residual X로부터 인과효과 τ를 추정하는 M_final입니다. 모델 학습 과정은 다음과 같습니다.Source: Causal Inference for The Brave and True두 번째 특징은 cross-fitting입니다. 이는 cross-validation과 유사한 개념으로, 데이터를 k-fold로 나누어 한 부분에서는 모델을 학습하고 다른 부분에서는 CATE 추정치를 계산합니다. 이를 통해 과적합(overfitting)을 방지할 수 있습니다.https://medium.com/media/b59c44a26a6bd2d58784ba8932cb2850/href“No Free Lunch”라는 말처럼, 모든 데이터에서 DML이 최선은 아닙니다. 하지만 성능뿐만 아니라 범용성을 고려했을 때, DML이 가장 적합하다는 결론을 내렸습니다. DML은 treatment와 outcome이 continuous이든 categorical이든 사용할 수 있고, Multiple treatments/outcomes을 처리할 수 있으며, 이들의 관계를 선형이 아닌 비선형으로 가정할 수 있는 유일한 모델이었습니다.DML의 한계점은 ‘no hidden confounder’ 가정을 만족해야 한다는 점입니다. 순수한 인과효과를 측정 가능하려면 모든 confounder가 데이터에 포함되어 있어야 합니다. 사실 이 가정은 DML뿐만 아니라 모든 causal inference 모델에 해당합니다. DML에 대한 자세한 내용은 책 [3][4] 과 DoubleML 문서 [5]를 참고해주시기 바랍니다.2.2 인과효과 추정(Estimation)Causal inference 문제는 ML의 지도 학습(supervised learning) 문제와 달리 ”정답을 알 수 없다” 는 차이점이 있습니다. 앞서 설명한 것처럼, causal inference의 목표는 outcome 자체를 예측하는 것이 아니라 인과 효과를 추정하는 것입니다. 그러나 인과 효과를 측정하려면 (1) treatment가 있을 때의 outcome과 (2) treatment가 없을 때의 outcome, 두 값을 모두 알아야 합니다. 예를 들어, 사용자 A의 (1) 마케팅 참여 시 결제 여부와 (2) 마케팅에 참여하지 않았을 때의 결제 여부를 알아야 합니다. 하지만 현실에서는 한 사람이 두 상황을 동시에 경험할 수 없습니다. 한 사람은 “마케팅에 참여하거나, 참여하지 않거나” 하나의 경험만 할 수 있습니다. 실제로 마케팅에 참여했다면, 참여하지 않았을 때의 결제 여부는 관측할 수 없는 것이죠. causal inference에서는 이러한 현실과 반대되는 상황을 반사실(counterfactual)이라고 합니다. Counterfactual은 모델 학습 뿐만이 아니라 인과 효과 추정과 평가 단계에서도 기존 ML과 다른 접근을 요구합니다. Counterfactual에 대한 더 자세한 내용은 강의 영상을 참고해주시기 바랍니다.Treatment가 binary일 경우, CATE의 수식과 인과 효과 추정 프로세스는 다음과 같습니다. 기존의 머신러닝 문제에서는 Y만 예측하지만, CATE 추정에서는 counterfactual도 예측해야 한다는 차이점이 있습니다.CATE = E[Y(T=1) − Y(T=0) | X]Sourec: https://arxiv.org/pdf/2109.12769Multiple treatments에서는 T=0, 1000, 2000 각각에 대해 CATE를 추정합니다. 모델로부터 각 treatment에 대한 score를 예측하고, treatment=0일 때의 score와의 차이를 계산하여 CATE를 구합니다. 더 자세한 이론은 논문 [2]와 EconML 코드을 참고해주시기 바랍니다.3. Offline 평가앞서 causal inference는 인과효과를 추정하는 문제이며, 현실에서는 counterfactual을 관측할 수 없어 정답인 인과효과를 알 수 없다고 설명했습니다. 그렇다면, 정답을 알 수 없는 상황에서 모델의 성능을 어떻게 평가할 수 있을까요? Causal inference에서는 여러 접근들이 있는데요, counterfactual을 control 그룹의 평균값으로 보는 접근이 있습니다. Binary outcome에 대한 대표적인 평가지표로 Area Under Uplift Curve (AUUC) 가 있습니다. AUUC는 Area Under the ROC curve(AUC) 와 매우 유사한 metric입니다. 차이점은 각 점수의 임계값마다 uplift를 계산한다는 점입니다. uplift는 treatment 그룹과 control 그룹(treatment=0)의 precision 차이를 계산합니다.Source: (2016) Causal Inference and Uplift Modeling A review of the literatureSource: https://medium.com/data-reply-it-datatech/uplift-modeling-predict-the-causal-effect-of-marketing-communications-24385fb04f2eAUUC를 통해 랜덤 모델 또는 다른 모델과의 성능 차이를 확인할 수 있습니다. AUUC에 대한 자세한 내용은 책 [7]과 causalml 코드를 참고해주시기 바랍니다.4. Policy 생성여기까지 진행했다면, 여러 treatment에 대한 개인화된 CATE 값을 확보했을 것입니다. 이제 ”여러 treatment 중 어떤 treatment를 선택해야 하는가? 마케팅 금액으로 얼마를 제공해야 하는가?” 에 대한 답을 찾기 위해 최적의 treatment를 선택하는 과정이 필요합니다.첫 번째로, CATE 값이 가장 큰 treatment를 선택했습니다 [2]. ‘Persuadables’ 고객일 확률이 높은 사용자를 선택하는 것이 타당한 접근이라 생각했습니다. 두 번째로, treatment 간의 CATE 차이를 고려했습니다. 마케팅에서는 큰 금액을 제공할수록 전환율이 높아지는 경향이 있었습니다. 한정된 마케팅 예산을 효율적으로 사용하기 위해, 가장 큰 금액의 treatment는 일부 사용자에게만 제공하도록 했습니다. 이를 위해 ‘CATE gap’ 룰을 휴리스틱하게 설정했습니다. CATE gap은 큰 treatment의 CATE와 작은 treatment 의 CATE 차이를 의미합니다. CATE gap이 큰 사용자에게는 더 큰 금액의 treatment를 할당했습니다.예를 들어, 사용자 A와 B의 CATE를 다음과 같다고 가정합니다:사용자A: CATE_A(T=1000) = 0.7, CATE_A(T=2000) = 0.8사용자B: CATE_B(T=1000) = 0.1, CATE_B(T=2000) = 0.8단순히 CATE만 고려하면 두 사용자 모두에게 2000원을 제공하는 것이 최선입니다. 하지만 1000원 마케팅 효과는 사용자 A는 CATE gap이 0.1로 작지만, 사용자 B는 0.7로 더 큰 값을 가집니다. 따라서 2000원을 제공해야 하는 한 명을 선택해야 한다면, gap이 큰 사용자 B를 선택하는 것이 더 비용효율적입니다. 사용자 A는 1000원을 제공해도 충분히 높은 효과를 기대할 수 있기 때문입니다.5. Online 평가실제 서비스에 마케팅을 적용하고 A/B 테스트를 진행했습니다. 마케팅 비용 최적화가 목표였으므로 비즈니스 KPI로 Cost Per Acquisition(CPA)을 선정했습니다. Control 그룹은 프로모션 금액을 랜덤으로 제공했습니다. Online 평가에서는 treatment 그룹과 control 그룹의 CPA를 비교하여 ”마케팅 비용이 얼마나 절감되었는지” 를 검증했습니다. Treatment와 control 그룹의 CPA를 계산한 뒤, treatment 노출 사용자 수에 control CPA를 곱하여 모델이 적용되지 않았을 경우의 전환 유저 수를 추정했습니다. 그리고 treatment 그룹의 총 비용과 control 그룹의 추정된 총 비용을 비교하여 모델이 비용 절감에 기여했음을 확인했습니다. 아래 표는 가상의 데이터로 계산한 예시입니다. CPA가 treatment 그룹은 1,429원, control 그룹은 1,643원으로 한사람당 마케팅 비용을 214원 절감 했음을 확인할 수 있습니다.후기지금까지 multiple treatments를 다룬 uplift modeling에 대해 살펴보았습니다. 프로젝트를 진행하면서 느낀 점은 confounder를 얼마나 잘 선정했느냐가 가장 중요하다는 것입니다. 아무리 causal inference 모델이 인과효과를 추정하더라도, 의미 있는 confounder가 포함되지 않으면 이는 일반적인 ML 모델과 다를 바가 없게 됩니다.저희 팀은 uplift modeling과 같은 causal inference뿐만 아니라, causal discovery를 통한 product analysis case 같은 의사결정에 도움이 되는 모델도 개발하고 있습니다. 기회가 된다면 causal discovery 관련 사례도 공유드릴 수 있길 기대해봅니다. 프로젝트에 도움을 주신 분들께 감사드리며, 긴 글 읽어주셔서 감사합니다.Reference[1] (2016) Causal Inference and Uplift Modeling A review of the literature[2] (2020) Uplift Modeling for Multiple Treatments with Cost Optimization[3] (2024) 인과관계 추론과 발견 with Python[4] https://matheusfacure.github.io/python-causality-handbook/22-Debiased-Orthogonal-Machine-Learning.html[5] https://docs.doubleml.org/stable/guide/basics.html[6] 2–1. 잠재적결과 프레임워크, Korea Summer Workshop on Causal Inference 2022[7] https://matheusfacure.github.io/python-causality-handbook/19-Evaluating-Causal-Models.html[8] https://medium.com/@playtika-tech-ai/analyzing-uplift-models-a4fbbf4c8ba9Uplift Modeling을 통한 마케팅 비용 최적화 (with Multiple Treatments) was originally published in NAVER Pay Dev Blog on Medium, where people are continuing the conversation by highlighting and responding to this story.

네이버 지도 내비게이션, 꽃길만 가자 — 좁은 길 탐지 모델
네이버 플레이스
네이버 지도 내비게이션, 꽃길만 가자 — 좁은 길 탐지 모델

네이버 지도 내비게이션, 꽃길만 가자 — 좁은 길 탐지 모델1. INTRODUCTION안녕하세요. NAVER Maps Directions의 김준태 입니다.저희 팀은 네이버 지도에서 내비게이션, 대중교통 길찾기 서비스 개발을 중심으로 좋은 품질의 서비스를 제공하기 위해서 빅데이터 수집, 분석, 모델링(통계, DL) 등도 수행하고 있는데요.이번에 소개드릴...

AI, 클라우드로도 바꾸지 못한 것: 엔터프라이즈 IT
삼성 SDS
AI, 클라우드로도 바꾸지 못한 것: 엔터프라이즈 IT

10년 전의 클라우드 컴퓨팅이나 현재의 생성형 AI 등은 모두를 바꿀 것처럼 보였지만, 실제로는 엔터프라이즈 IT 지출의 대부분이 여전히 온프레미스 방식에 머무르고 있습니다. AI를 도입하려는 기업이라면, 그것이 기존의 데이터 인프라, 프로그래밍 언어 등과 어떻게 연결될 수 있는지를 고민해야 합니다.

상태 관리 라이브러리 vanilla-store
네이버 페이
상태 관리 라이브러리 vanilla-store

상태 관리 라이브러리 vanilla-store를 소개합니다!안녕하세요. 내자산&회원FE팀 김현석입니다. 리액트를 사용하는 개발자들에게 상태 관리는 항상 중요한 고민거리입니다. 갈수록 커지고 복잡해지는 앱은 상태 관리를 더 어렵게 만들고 있으며 이를 효과적으로 다루기 위해 상태 관리 라이브러리를 사용해 개발하는 경우가 많습니다. Redux, MobX, Recoil, Jotai 등 많은 상태 관리 라이브러리들이 있고 네이버페이에서도 각 팀의 필요에 맞는 라이브러리를 선택해 개발하고 있습니다.제가 속한 팀에서는 mobx를 주로 상태 관리라이브러리로 사용했습니다. mobx는 다른 라이브러리에 비해 보일러 플레이트 코드가 적고, observer로 컴포넌트를 감싸주기만 하면 상태가 변했을 때 알아서 업데이트를 수행합니다. 이러한 간결함 덕분에 비즈니스 로직에 집중할 수 있는 장점이 있었고 많은 서비스들을 mobx를 사용해 개발했었습니다.그러던 중 담당 업권 서비스가 복잡해지면서 별도 레포로 이관하게 되었는데 이 과정에서 불필요한 의존성을 줄이자는 기술적인 목표를 설정했습니다. 상태 관리 라이브러리도 그 대상으로 최대한 react-api를 활용하면 이를 대체할 수 있지 않을까라는 생각으로 작업을 진행했습니다.생각보다 많은 부분들을 react-api를 통해 대체할 수 있었습니다. 하지만 작업을 진행하면서 mobx의 부재가 느껴지는 부분이 존재했고 결국 상태 관리 라이브러리의 기능이 필요하다는 결론을 내렸습니다. 다만 mobx를 다시 도입할 정도로 큰 문제는 아니었기에 지금 우리에게 필요한 기능만을 가진 간단한 상태 관리 라이브러리를 만들어 적용하면 어떨까라는 생각으로 vanilla-store를 만들게 되었습니다. 아래 글을 통해서 vanilla-store의 내부 구현, 사용 예제, 실제 사용 예시를 소개합니다.useSyncExternalStore본격적으로 vanilla-store에 대해서 소개하기에 앞서서 리액트의 상태 관리에 대해서 살펴보겠습니다.리액트 생태계 내부에서의 상태 관리는 우리가 익히 알고 있는 `useState, this.setState`등을 통해 이루어집니다. 상태 값이 바뀌면 해당 useState를 포함한 컴포넌트가 리렌더링되어 변경된 상태를 형상에 반영합니다.그럼 리액트 생태계 외부에서는 어떻게 상태 관리를 하면 될까요? 아래 코드처럼 외부에 상태 관리를 해도 될까요?let count = 0function Component() { function handleClick() { count += 1 } return <div onClick={handleClick}>{count}</div>}짐작하셨겠지만 전역변수 count가 바뀌어도 형상에는 반영되지 않습니다. 리액트는 아래 경우에서만 리렌더링을 트리거 합니다.- 클래스 컴포넌트에서 `this.setState`,`forceUpdate` - 함수 컴포넌트에서 `useState`, `useReducer` - 컴포넌트의 props가 변경되는 경우- 부모 컴포넌트가 리렌더링 된 경우리액트 생태계 외부의 상태 값이 바뀌어도 리액트 업데이트 큐에 반영되지 않기에 형상이 바뀌지 않는 것이죠. 외부의 상태가 바뀌었을 때 리액트에 이를 알려주고 생명주기에 맞게 업데이트해주는 무언가가 필요합니다. 리액트는 이를 지원하기 위해 useSyncExternalStore 훅을 제공합니다.useSyncExternalStore는 react18에서 새롭게 도입된 훅으로 외부 스토어와의 동기화를 간편하게 처리할 수 있게 해줍니다.useSyncExternalStore( subscribe: (callback) => Unsubscribe getSnapshot: () => State getServerSnapshot?: () => State) => StateuseSyncExternalStore는 세 매개변수를 받습니다.subscribe는 callback을 받아 등록하고 cleanup으로 등록한 callback을 제거하는 함수입니다. 말이 좀 복잡한데 매개변수로 넘어오는 callback이 리액트의 리렌더링을 트리거 하는 요소로 외부 스토어에서 이를 저장했다가 상태가 변경될 때 callback을 실행해 리액트와 싱크를 맞출 수 있게 합니다.getSnapshot은 렌더링 된 이후 값이 변경되었는지, 문자나 숫자처럼 immutable 한 값인지 확인하는데 사용됩니다. useSyncExternalStore은 이 확인이 끝난 immutable 한 값을 반환합니다.옵셔널 하게 받는 getServerSnapshot은 이름처럼 getSnapshot과 같지만 서버 렌더링의 안정성을 보장하기 위해 사용합니다.useSyncExternalStore는 또한 react18에서 concurrent 렌더링 사용 시 발생할 수 있는 ui 불일치 문제 tearing 을 해결해 줍니다.https://github.com/reactwg/react-18/discussions/69#discussion-3450021 / react disscussionreact18 이전까지는 위 그림처럼 동기적 렌더링만 지원되었습니다. 따라서 바뀐 상태에 맞게 일관된 형상을 노출할 수 있었습니다.https://github.com/reactwg/react-18/discussions/69#discussion-3450021 / react disscussion이와 다르게 concurrent 렌더링은 다른 우선순위가 높은 작업을 먼저 수행하기 위해 현재 렌더링 작업을 일시 중단시킬 수 있습니다.위 그림에서 모든 노드가 파란색이 되기 전 렌더링이 중단되고 그사이 바뀐 빨간색으로 모든 노드의 색이 불일치하는 걸 볼 수 있습니다. 렌더링이 비동기적으로 이루어지고 외부 스토어의 값이 그 사이에 변경되었을 때 문제가 발생하게 되는 것입니다.useSyncExternalStore는 React 18의 concurrent 렌더링 환경에서도 외부 스토어와의 동기화를 보장합니다. 이는 React 18 이전의 동기적(blocking) 렌더링과 유사한 일관성을 제공하여 tearing 문제를 해결합니다.내부적으로 변경사항을 dom에 적용하기 직전에 getSnapshot을 한 번 더 호출하여 처음 호출했을 때와 다른 값을 반환하면 (tearing 문제가 발생했다면) 업데이트를 다시 처음부터 시작해 일관된 형상을 노출할 수 있게 합니다.useSyncExternalStore는 react 18의 concurrent 렌더링을 상정하고 만들어졌기에 그 이전 버전에서는 사용할 수 없습니다. 문제는 react 18로의 전환은 breaking change가 많기 때문에 라이브러리 개발자의 부담이 크다는 것입니다. 이를 해결하기 위해 리액트 팀에서는 react 17 이하에서도 사용할 수 있는 useSyncExternalStore shim 패키지를 제공해 점진적인 마이그레이션을 지원합니다.상기한 이점들로 인해 recoil, zustand 등 많은 상태 관리 라이브러리들에서도 내부에 useSyncExternalStore를 사용하는 것을 볼 수 있습니다. 그리고 이제 소개할 vanilla-store도 useSyncExternalStore를 사용해 구현되었습니다!vanilla-store만들게 된 배경mobx를 제거하고 react-api useContext, useReducer 등을 사용해 상당 부분 상태 관리 라이브러리를 대체할 수 있었지만 동시에 문제점도 나타났습니다. 나빠진 가독성과 과도해진 보일러 플레이트 코드 작성이었습니다.컴포넌트에서 useContext를 사용하려면 반드시 부모 노드에 provider가 있어야 합니다. 여러 컴포넌트들에서 useContext가 사용된다면 그들 간의 공통 부모 노드에 provider가 위치 해야 하고 경우에 따라 최상위 노드까지 provider의 위치가 올라가게 됩니다.팀에서 nextjs를 사용 중이었기 때문에 여러 페이지에 동일한 provider를 적용하는 일이 발생했고, 반복을 줄이기 위해 _app 파일로 provider를 올리면 getInitialProps가 비대해지는 문제가 발생했습니다.또한 페이지에 따라 불필요한 로직, provider가 _app 파일에 추가되는 것이니 성능적으로도 좋을 것이 없었고 이 코드가 현재 사용 중인지 로직을 따라가기에도 어려움이 생겼습니다. 적은 보일러 플레이트 코드를 가지고 여러 컴포넌트에서 전역 상태에 접근할 수 있는 무언가가 필요했습니다. 다시 상태 관리 라이브러리를 사용하면 해결되는 문제였지만 우리에게 필요한 기능에 비해 라이브러리가 컸고 이 정도 기능이라면 만들어서 써도 되지 않을까 하는 호기심에 vanilla-store를 만들게 되었습니다.내부 구현과 인터페이스 소개vanilla-store github linkvanilla-store는 스토어 생성 함수 createVanillaStore와 사용 hooks useStore로 구성되어 있습니다.createVanillaStore는 매개변수로 상태 값과 persist 옵션을 옵셔널로 받습니다. 이중 옵션은 아래에서 다루고 먼저 상태 값부터 보겠습니다const createVanillaStore = <State>(initialState: State, options?: Options<State>): VanillaStore<State> => { …}const initState = { count : 0, name : 'npay'}const store = createVanillaStore(initState)initialState는 store로 관리하려는 상태의 초깃값을 의미합니다. 이때 초깃값 선언과 createVanillaStore 실행은 컴포넌트 외부와 같이 반복적으로 실행될 여지가 없는 독립적인 환경에서 수행해야 합니다. 그래야 올바르게 초기화된 하나의 객체가 생성될 수 있기 때문입니다. createVanillaStore 내부를 좀 더 자세히 살펴보겠습니다.createVanillaStoreconst createVanillaStore = <State>(initialState: State, options?: Options<State>): VanillaStore<State> => { // 초기 상태값 할당 let state = initialState // set실행시 수행할 callback 함수 저장 set. 여기에 useSynExternalStore의 callback이 저장됨. const callbacks = new Set<() => void>() // 상태값 반환 함수. useSyncExternalStore의 snapShot으로 전달 const get = () => state // 상태값 변경 함수. useState처럼 상태값 혹은 함수를 받음 const set = (nextState: State | ((prev: State) => State)) => { // 서버에서의 사용을 허용하지 않는 가드문 if (typeof window === 'undefined') { throw new Error('This function is not available in Server side.') } // 상태값 혹은 함수 여부에 따라 다르게 state 값 갱신 state = typeof nextState === 'function' ? (nextState as (prev: State) => State)(state) : nextState // set에 저장되어있는 모든 callback 함수 실행. 이 동작으로 리액트 리렌더링이 트리거 callbacks.forEach((callback) => callback()) return state}… // useSyncExternalStore에 전달할 subscribe 함수 const subscribe = (callback: () => void) => { // useSyncExternalStore로 부터 전달받은 callback을 set자료구조에 저장 callbacks.add(callback) return () => { // cleanup 함수에서 등록했던 callback 제거 callbacks.delete(callback) } } return {get, set, subscribe, persistStore}}createVanillaStore는 상태 snapShot을 반환하는 get, 상태를 갱신하는 set, useSyncExternalStore에 전달할 subscribe, persist option 활성화 시 사용되는 persistStore 네 가지 값을 반환합니다.subscibe 함수는 useSyncExternalStore에 전달되어 컴포넌트 리렌더링을 트리거 하는 함수를 callback으로 받게 됩니다. 그리고 이를 callbacks 자료구조 Set에 등록하고 set 함수가 실행될 때마다 등록된 모든 callback 함수들을 실행합니다.즉, 상태 값이 바뀌는 set 함수가 실행될 때 컴포넌트 리랜더링을 트리거 하여 바뀐 상태로 형상을 갱신하게 됩니다. 이렇게 해서 외부 스토어와 리액트 간 싱크를 맞출 수 있게 됩니다.useStoreconst useStore = <State>(store: VanillaStore<State>, initialValue?: State) => { // useSyncExternalStore의 명세에 맞게 subscribe, snapShot을 전달하고 immutable한 상태값을 반환 const value = useSyncExternalStore(store.subscribe, store.get, () => initialValue || store.get()) … return [value, store.set] as const}useStore는 createVanillaStore에서 반환하는 subscribe, snapshot을 useSyncExternalStore에 전달하는 함수입니다. useSyncExternalStore를 통해 안전하게 관찰되는 상태 값과, 스토어에서 전달받은 set 함수를 반환합니다.기본 사용 예시// 초기 상태값 정의const initState = { count : 0, name : 'npay'}// createVanillaStore로 vanillaStore객체 생성const store = createVanillaStore(initState)export default function MyAppCount() { // useStore훅에 vanillaStore객체를 전달해 immutable한 상태값, setter함수를 반환 const [state, setState] = useStore(store) const handleClick = () => { // 상태값 변경 파라미터로 함수 전달. count 필드 값 변경 setState((prev) => ({…prev, count+1})) } return <div onClick={handleClick}>{state.count}</div>}export default function MyAppName() { const [state, setState] = useStore(store) const handleClick = () => { // name 필드 값 변경 setState((prev) => ({…prev, name:'point'})) } return <div onClick={handleClick}>{state.name}</div>}우리에게 익숙한 useState처럼 useStore를 통해 상태에 접근하고 변경할 수 있게 됩니다. 또한 useContext처럼 provider의 제약 없이 어떤 컴포넌트든 useStore를 통해 자유롭게 상태에 접근할 수 있습니다.다만 여기서 한 가지 아쉬운 부분이 생깁니다. 위 예시에서 MyAppCount 컴포넌트의 handleClick이 실행되면 count 값이 변경되면서 리렌더링이 트리거 될 것입니다.문제는 count를 사용하지 않는 MyAppName 컴포넌트 역시 store에서 관찰하는 상태가 변경되었기 때문에 마찬가지로 리렌더링이 트리거 됩니다. MyAppName 컴포넌트 입장에서는 불필요한 리렌더링이 실행되는 것이죠.이 문제를 해결하기 위해 vanilla-store에서는 useStoreSelector hooks를 준비했습니다.useStoreSelectorfunction useSyncExternalStoreWithSelector<Snapshot, Selection>( subscribe: (onStoreChange: () => void) => () => void getSnapshot: () => Snapshot, getServerSnapshot: undefined | (() => Snapshot), // 관찰하고 싶은 필드를 특정하기위한 필터링 함수 selector: (snapshot: Snapshot) => Selection, // 필드가 변경되었는지 비교하기위한 함수. 별도로 정의해주지 않으면 내장된 shallowEqual 함수로 비교연산 수행 isEqual: (a: Selection, b: Selection) => boolean = shallowEqual, ): Selection { // selector를 통해 전체 상태 값 중 관찰하고자하는 필드를 특정해 초기값 할당 const initialSelection = selector(getSnapshot()) // 위에 특정된 값을 리렌더링 이후 비교하기위해 useRef에 저장 const stateRef = useRef<Selection>(initialSelection) // useSyncExternalStore는 상태가 변경되면 변경된 상태값을 반환 const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) // useSyncExternalStore에서 반환된 상태값을 필터링 해 관찰하고 싶은 필드를 특정 const selection = selector(snapshot) // useSyncExternalStore 수행 전, 후 필드 값을 비교 if (!isEqual(selection, stateRef.current)) { // 만약 필드값이 변경되었다면 useRef 값 변경 stateRef.current = selection } // useRef의 current가 갱신된 경우에 반환값이 바뀌어 컴포넌트 리렌더링을 트리거 return stateRef.current}const useStoreSelector = <State, Value>( store: VanillaStore<State>, selector: (state: State) => Value, options?: {initialStoreValue?: State; isEqual?: (a: Value, b: Value) => boolean},) => { const {initialStoreValue, isEqual} = options || {} // useSyncExternalStore에 selector, isEqual만 파라미터로 추가됨 const value = useSyncExternalStoreWithSelector( store.subscribe, store.get, () => initialStoreValue || store.get(), selector, isEqual, ) return [value, store.set] as const}코드가 다소 복잡해 보이지만 내용은 간단합니다. useStore처럼 useSyncExternalStore를 통해 상태를 관찰하고 리렌더링을 트리거 하지만 selector를 통해 관찰하고자 하는 특정 필드를 선택하고 useRef를 통해 이를 저장합니다.이후 상태가 바뀌어 useSyncExternalStore의 반환값이 바뀌면 마찬가지로 selector를 통해 특정 필드를 선택하고 useRef의 값과 비교해 해당 필드가 변경된 경우에만 useRef의 값을 갱신합니다.이때 비교 함수 isEqual은 리액트의 shallowEqual처럼 동작하도록 구현했습니다. 비교하려는 필드가 중첩된 객체여도 key와 참조 값 value만을 비교할 뿐 하위 객체를 모두 순회해서 내부 필드가 바뀌었는지 까지는 판단하지 않습니다.조금 부정확할 순 있겠지만 비용이 큰 비교 연산을 절약해 성능상의 이점을 가져가기 위함입니다.useStoreSelector 사용 예시// 초기 상태값 정의const initState = { count : 0, name : 'npay'}// vanillaStore 객체 생성const store = createVanillaStore(initState)export default function MyAppCount() { // vanillaStore객체와 selector 전달. selector함수에서 count 필드를 관찰 // MyAppCount 컴포넌트에서는 전체 상태값 중 count 필드가 변경될 때에만 리렌더링을 트리거하게함 const [count, setState] = useStoreSelector(store, (state) => state.count) const handleClick = () => { // count의 상태값을 변경 setState((prev) => ({…prev, count+1})) } return <div onClick={handleClick}>{count}</div>}export default function MyAppName() { // selector함수에서 name 필드를 관찰 // MyAppName 컴포넌트에서는 전체 상태값 중 name 필드가 변경될 때에만 리렌더링을 트리거하게함 const [name, setState] = useStoreSelector(store, (state) => state.name) const handleClick = () => { // name의 상태값을 변경 setState((prev) => ({…prev, name:'point'})) } return <div onClick={handleClick}>{name}</div>}앞서 useStore를 사용했을 때 불필요한 리렌더링을 만들었던 부분을 useStoreSelector를 통해 개선한 모습입니다.useStoreSelector의 두 번째 인자로 selector 함수를 넘겨서 컴포넌트에서 관찰하려는 값을 한정하는 것을 볼 수 있습니다. 이제 MyAppName 컴포넌트는 count 상태 값이 바뀌어도 불필요한 리렌더링 수행하지 않게 됩니다!persist optionvanilla-store의 useStore에는 localStorage, sessionStoreage를 사용하는 persist option을 지원합니다. 이 기능은 페이지 이동, 새로고침을 해도 상태 값을 유지해야 하는 경우에 유용합니다.const createVanillaStore = <State>(initialState: State, options?: Options<State>): VanillaStore<State> => { // 상태값 초기화 및 할당 let state = initialState // localStorage or sessionStorage 저장소를 할당할 변수 let persistStore: Persistent<State> | null = null // 상태값 변경 시 수행할 callback함수 저장 set. // persistent옵션을 활성화한 경우 persistStore에 변경된 상태값을 할당하는 callback이 저장됨 const callbacks = new Set<() => void>()… // persist 옵션이 있는 경우에만 실행 if (options?.persist) { // persistent store에 저장된 value에 접근하기위한 key값 할당 const key = options.persist.key // 옵션에 전달한 타입이 localStorage인 경우 아래 조건문 실행 if (options.persist.type === 'localStorage') { // LocalStoragePersist 인스턴스 할당. 내부에 구현된 window.localStorage 사용 편의를 위해 만든 클래스 persistStore = new LocalStoragePersist(key, initialState, options.persist.typeAssertion) // callback으로 localStorage에 변경된 상태값을 반영하도록 하는 함수 저장. callbacks.add(() => { if (persistStore) { persistStore.value = get() } }) } // 옵션에 전달한 타입이 sessionStorage인 경우 아래 조건문 실행 if (options.persist.type === 'sessionStorage') { // SessionStoragePersist 인스턴스 할당. 내부에 구현된 window.sessionStorage 사용 편의를 위해 만든 클래스 persistStore = new SessionStoragePersist(key, initialState, options.persist.typeAssertion) // callback으로 sessionStorage에 변경된 상태값을 반영하도록 하는 함수 저장. callbacks.add(() => { if (persistStore) { persistStore.value = get() } }) } }… return {get, set, subscribe, persistStore}}// persist 옵션으로 type, key 전달// isStoreValue : 저장될 값 vanildation하는 함수. persistStorage에 저장하기 전 타입 에러 방지를 위해 검증하는 용도로 사용const store = createVanillaStore(initState, persist: {type: 'sessionStorage', key: 'sessionStorageKey', typeAssertion: isStoreValue})options에서 type을 통해 어떤 persist 저장소를 사용할지 선택하고 callbacks에 저장소 값을 변경하는 함수를 등록합니다. 이를 통해 상태가 변경되면 등록된 callback이 실행되어 컴포넌트 리렌더링 트리거와 함께 persist 저장소의 값을 갱신하게 됩니다.실제 사용하고 있는 서비스 예시vanilla-store는 네이버페이 내 자산에 수입 지출 서비스에 적용되어 있습니다. 수입 지출 서비스 내에서도 많은 부분들에 사용되고 있는데 이중 session storage를 사용하는 예시를 소개해드리려 합니다.수입 지출 서비스에는 여러 거래내역이 묶인 복합결제 거래내역이 있는데 클릭 시 세부 거래내역을 노출합니다. 거기에 새로고침을 해도 세부 내역 노출을 유지해야 하는 흔히 볼 수 있는 스펙입니다. 이를 useStore의 persist 옵션을 사용해 구현했습니다const sessionStorageKey = 'sessionStorageKey'type UnfoldStatus = Record<string, boolean>// 상태 초기 값. key로 거래내역의 id, value로 접힘여부를 판단하는 boolean값을 사용const initValue: UnfoldStatus = {}// vanillaStore persist 옵션 넣어서 생성export const unfoldStatusStore = createVanillaStore(initValue, { // sessionStorage 사용, sessionStorageKey를 key로 sessionStorage에 저장된 값 접근 persist: {type: 'sessionStorage', key: sessionStorageKey, typeAssertion: isStoreValue},})function ItemComponent() { const [status, setStatus] = useStore(unfoldStatusStore) …}해당 묶음 거래내역의 id를 key로 가지는 상태를 만들어 sessionStorage에 저장해 key가 있다면 세부 내역을 노출하도록 구현했습니다.복합결제 거래내역복합결제의 세부 내역까지 노출된 형상개발자 도구를 통해 클릭에 따라 sessionStorage 값이 바뀌는 것을 볼 수 있습니다.예시의 수입 지출 외에 네이버페이 내 자산의 다양한 서비스에도 vanilla-store가 적용되어 있습니다!다른 상태관리와 비교vanilla-store의 가장 큰 특징은 가벼움에 있습니다. 배경 자체가 기본 react api의 약간의 아쉬움을 보완하는 것에서 출발했기 때문에 간결한 전역 상태 관리라는 기능에 집중해 번들사이즈가 작습니다.bundle phobia를 통해 상태 관리 라이브러리 recoil, jotai 와 비교해 봤을 때 vanilla-store의 번들 사이즈가 훨씬 작음을 볼 수 있습니다.naverpay/vanilla-storerecoiljotai또한 직접 내부를 js로 구현했기 때문에 별도 package.json의 dependencies가 없습니다. 이 때문에 다른 패키지와의 의존성 문제에서 자유로울 수 있습니다.다만, 리액트 환경만을 지원하고 위에 소개한 useSyncExternalStore를 내장하고 있기에 peer dependencies로 react 18 조건을 가지고 있는 부분은 주의해야 합니다.앞으로 더 개발할 것들실 서비스에서 vanilla-store를 적용하면서 몇 가지 아쉬운 부분이 있었습니다. 개발 과정에서 스토어 내부의 상태를 추적하기 번거로웠던 것입니다. 지금의 vanilla-store는 값이 어떻게 바뀌는지 확인하려면 직접 console.log를 찍어봐야 알 수 있습니다.작은 규모로 사용할 때는 흐름을 따라가기 쉽지만 크고 복잡한 서비스에서는 이 부분이 아쉬울 수 있겠다는 생각을 했습니다. 그래서 persist만 있는 options에 디버깅 옵션을 추가할 예정에 있습니다.또 하나의 아쉬운 점은 ssr 지원을 하지 않는 것입니다. 저희 팀 서비스들은 대부분 nextjs의 ssr을 적극적으로 사용하고 있는데 vanilla-store는 처음엔 클라이언트 사이드에서만 사용하는 것을 상정하고 제작했기 때문에 ssr 시점에는 동작하지 않습니다.이 부분을 보완하기 위한 패키지 내부에서의 provider 제공 작업도 예정되어 있습니다. 위 예시들뿐만 아니라 vanilla-store를 사용하면서 부족한 부분들이 보이면 계속해서 보완해 나갈 예정입니다.마치며지금까지 vanilla-store에 배경과 구현, 실제 적용된 예시들을 살펴봤습니다. 서비스 스펙이 아닌 기술을 위한 개발을 해보고 실 서비스에 적용해 보는 건 색다른 재미를 줬던 것 같습니다. 특히 간단한 상태 관리 기능에만 집중하긴 했지만 생각보다 쉽게 기존의 상태 관리 라이브러리를 대체할 수 있다는 것이 신기했습니다.여기에는 몇 가지 이유가 있다고 생각하는데 기존에도 서비스에서 상태 관리 라이브러리의 일부 간단한 기능만을 사용했다는 점, useSyncExternalStore가 복잡해질 수 있는 리액트와 외부 저장소 간의 싱크 문제를 매우 간단하게 처리해 준 점입니다.저와 비슷한 고민을 하는 분이 있다면 vanilla-store가 작게나마 도움이 되었기를 희망하면서 글을 마치겠습니다. 네이버페이는 서비스 개발뿐만 아니라 다양한 기술적 도전들에 열려있습니다. vanilla-store도 팀에서 다양한 개발을 지원하고 독려하는 분위기가 있어서 끝까지 개발을 추진할 수 있던 것 같습니다. 이런 도전적 문화와 사람들 속에서 함께 성장할 동료를 찾고 있습니다! https://recruit.naverfincorp.com/상태 관리 라이브러리 vanilla-store was originally published in NAVER Pay Dev Blog on Medium, where people are continuing the conversation by highlighting and responding to this story.

올리브영에서는 프론트엔드 개발자들이 이런 고민을 하는군요?
올리브영
올리브영에서는 프론트엔드 개발자들이 이런 고민을 하는군요?

😲 "어? 올리브영에도 개발자가 있나요?" 🤔: 올리브네트웍스를 말씀하시는 건가요? 올리브영은 다 SI로 개발하는 거 아니었어요? 🫢: 우와, 올리브영에 개발자가 생각보다 많네요! 그것도 15…

PQC로의 여정: 양자 컴퓨터 시대에 데이터 지키기
라인
PQC로의 여정: 양자 컴퓨터 시대에 데이터 지키기

들어가며 안녕하세요. Security R&D 팀의 한주홍입니다. 저희 팀은 LY 그룹 서비스의 전반적인 보안을 강화하기 위해 다양한 보안 기술을 연구하고 개발 및 컨설팅하는 업무를...

컬리의 Virtual 물류 센터
마켓컬리
컬리의 Virtual 물류 센터

Picking 공정 시뮬레이션의 구축부터 활용까지

GS SHOP 패션 검색의 진화, Amazon Bedrock 멀티모달 기반 패션 검색 시스템 구현 사례
GS리테일
GS SHOP 패션 검색의 진화, Amazon Bedrock 멀티모달 기반 패션 검색 시스템 구현 사례

패션상품검색 시스템 개선 배경 GS SHOP은 국내 최대 규모의 온라인 패션 플랫폼으로, 700만 개가 넘는 방대한 양의 패션 상품 데이터를 보유하고 있습니다. 이러한 대규모 데이터에서 고객이 원하는 상품을 빠르고 정확하게 검색할 수 있도록 하는 것은 온라인 쇼핑 경험을 향상시키는 데 있어 필수적입니다. 기존에는 상품 이미지와 상품명, 카테고리 정보 ...

29CM QA팀은 테스트 자동화 사용률을 극적으로 상승 시키기 위해 무엇을 바꿨을까?
29cm
29CM QA팀은 테스트 자동화 사용률을 극적으로 상승 시키기 위해 무엇을 바꿨을까?

29CM QA팀도 테스트 자동화를 사용하시는 타 QA분들처럼 CI/CD(저희는 Jenkins를 사용합니다) 툴을 사용해서 테스트 자동화를 수행하고 있습니다.테스트 자동화를 위해 29CM앱의 전반적인 주요 기능을 검증 할 수 있도록 E2E 시나리오 28개를 (33개였으나 수행불가 시나리오제외)선정해놓고 APP 배포 전에 그것을 모두 수행하여 검증이 진행...

AI로 강화된 사이버보안
삼성 SDS
AI로 강화된 사이버보안

이 글에서는 생성형 AI의 보안 양면성에 대해 논하고 있습니다. 생성형 AI를 통해, 해커는 악성코드를 개발하거나 기업에 민감한 정보를 탈취하는 데 활용할 수 있고, 반대로 기업은 이러한 이상 행동을 모니터링하는 등 예방 활동에 이용할 수도 있습니다.

Redis Vs Mongo DB By Item View Count (이 상품 몇명이 보고 있어요)
gmarket
Redis Vs Mongo DB By Item View Count (이 상품 몇명이 보고 있어요)

안녕하세요 저는 VI Engineering 팀 김윤제입니다.Gmarket Mobile Web Vip(View Item Page = 상품 상세)를 담당하고 있는 Backend Engineer 입니다. 이번 블로그에서는 개인적으로 상품 상세 페이지에 넣고 싶었던현재 이 상품 몇 명이 보고 있어요 기능을 혼자 공부하며 개발해보는데 있어서 어떻게 설계를 해야...

LLM Application 구축 도전기 (feat. 소중한 고객님들의 리뷰) - 1부
마켓컬리
LLM Application 구축 도전기 (feat. 소중한 고객님들의 리뷰) - 1부

Prompt Engineering을 활용한 비정형 데이터 검수 실험

하이퍼커넥트 그룹콜 미디어 서버 인프라를 소개합니다
하이퍼커넥트
하이퍼커넥트 그룹콜 미디어 서버 인프라를 소개합니다

안녕하세요, 하이퍼커넥트 Media Lab의 Media Server Team에서 Media Server Engineer로 일하고 있는 Simon.Y 입니다. 저희 Media Server Team은 “사람들이 제약 없이 모여서 소통할 수 있도록 안정적인 연결을 제공하고 미디어 품질을 높이기 위해 노력한다”는 미션을 가지고 있습니다. 이 목표 아래, 대규...

대규모 마이크로서비스 구축 및 실행 사례: 프라이스라인(Priceline)
삼성 SDS
대규모 마이크로서비스 구축 및 실행 사례: 프라이스라인(Priceline)

이 사례는 프라이스라인의 CTO 마틴 브로드벡이 직접 작성했습니다. 프라이스라인은 12요소 방법론, 모노리포 접근 방식, 컨테이너 기반 마이크로서비스 등을 활용하여 소프트웨어 개발 방식을 혁신하고 있으며, 이러한 변화를 통해 고객 우선 접근 방법을 성공적으로 구현하고 있습니다

기업의 IT 구조 혁신: 기업 애플리케이션의 경량화 전략
삼성 SDS
기업의 IT 구조 혁신: 기업 애플리케이션의 경량화 전략

이 글에서는 모놀리식 아키텍처의 한계를 극복하고 MSA로 전환하는 과정에서 고려해야 할 핵심 전략을 다루고 있습니다. 경량화의 주요 개념을 설명하고, 이를 통해 기업이 개발 효율성과 확장성을 동시에 달성할 수 있는 방안을 제시합니다.

O4O 경험을 설계합니다 - 우리동네GS Product Manager
GS리테일
O4O 경험을 설계합니다 - 우리동네GS Product Manager

      GS리테일에서 Product Manager로 일하고 있는 강은영 매니저님을 만나 이야기를 나누어 보았습니다.     안녕하세요 매니저님, 먼저 O4O기획팀에 대해 소개 부탁드립니다.     O4O(Online for Offline)는 오프라인에서의 고객 경험을 온라인으로 확장하고...

FIDO2 클라이언트 SDK 오픈소스 소개
라인
FIDO2 클라이언트 SDK 오픈소스 소개

들어가며 안녕하세요. Security R&D 팀에서 FIDO2 클라이언트 개발을 담당하고 있는 김도연, 김영현입니다. 공개 키 암호화를 기반으로 한 FIDO는 패스워드나 SMS O...

AWS Bedrock과 Claude 3.5 Sonnet을 활용한 자동 상품 이미지 검수 시스템 구축기
올리브영
AWS Bedrock과 Claude 3.5 Sonnet을 활용한 자동 상품 이미지 검수 시스템 구축기

안녕하세요! 올리브영 카탈로그 서비스개발팀에서 AI와 씨름 중인 루시🐰입니다. "이미지 검수에 AI…