안녕하세요, 누리미디어 프론트엔드 개발자입니다. 저는 누리미디어에 입사 후, 첫 과제로 AI 검색 서비스의 프론트엔드 개발을 맡게 되었어요.이 글에서는 “AI 검색“ 프로젝트의 프론트엔드 개발 과정을 이야기해보려 합니다.
제목없음
AI 검색 서비스의 시작 - 프론트엔드 기술스택 정하기
"ChatGPT" 많이 들어보셨죠? “AI 검색“ 프로젝트는 “ChatGPT”와 비슷해요.
검색어를 입력하면, AI가 디비피아에 저장된 논문들과 정보들을 찾아서 보여주는 서비스입니다. "AI 검색"은 모든 걸 새로 만들어야 하는 신규 프로젝트라서 프로젝트 환경 설정부터 프레임워크, 라이브러리 등을 모두 정해야 하는 상황이었어요. 그중 프론트엔드 개발을 하기 위해 가장 먼저 정해야 했던 건 React와 Vue 중에 어떤 것을 사용할지 정하는 것이었습니다.
누리미디어 합류 과정의 인터뷰에서 현재 사용하고 있는 프론트엔드 기술이 어떤 것인지 이야기를 잠깐 나눴었어요. 누리미디어의 서비스는 여러 가지가 있지만 프론트엔드 프레임워크를 사용한 프로젝트는 “싸이티지“라는 서비스가 있고 Nuxt(Vue3)로 개발이 되어 있다고 말씀해 주셨어요.
(싸이티지 웹 구축 후기를 보고 싶으시다면? 👉 바로가기)
저는 Vue와 React 모두 친숙하고 충분히 능숙하게 사용이 가능하지만 Vue 보다는 React를 더 선호하는 편이에요. 그래서 인터뷰할 때 이런 저런 이유들을 말씀드리면서 React 도입에 대한 이야기를 넌지시 했었죠. 보통 기존에 사용하는 기술이 있는 상태에서 그 기술을 변경하는 건 쉽지 않기 때문에 “이런 저런 이유로 고민해봐야 할 것 같다“ 등의 답변을 생각했는데, 예상 밖의 답변을 해주셨어요. “만약 React 도입이 서비스와 협업, 채용 등에 도움이 된다면 충분히 도입이 가능하다” 라는 대답을 해주셨어요. 또한 원하는 기술들이 있을 때, 팀과 상의 후에 문제가 없고 사용해야 할 이유가 마땅하면 기술 도입에 대한 제한은 없다고 이야기를 해주셨습니다.
그런 팀의 성격이나 문화가 너무 마음에 들었어요. 이전에는 여러 이유로 인해 사용하고 싶던 기술들을 사용하지 못한 적이 꽤 있었거든요.
그래서 저는 입사 후 AI 검색 프론트엔드 기술스택을 정할 때 React 도입을 제안했습니다.
(Vue가 좋지 않다는 건 절대 아니에요!) 이유는 여러 가지가 있었어요. 우선 프론트엔드 생태계에서 Vue 보다는 React가 압도적으로 많이 사용되고 있어요. 많은 사람들이 사용한다는 것은 그만큼 커뮤니티가 방대하고, 데이터가 많이 있기 때문에 문제가 생겼을 때 보다 쉽게 정보를 얻을 수 있어요. 그리고 회사 입장에서는 채용도 더욱 수월하죠! 기술적으로 바라보았을 때 Vue와 React의 성능은 크게 차이가 없다고 생각해요. 다만 발전의 속도가 React가 조금 더 빠른 편이라고 저는 생각하고 있어요. React Query, Server Component, Server Action 등등… React가 새롭고 뛰어난 기능들을 꾸준히 업데이트 해주더라구요. 등등 여러가지를 고려 했을 때, React로 프로젝트를 설정하는 게 더 장점이 많을 것이라 생각했어요.
이런 이유들을 바탕으로 팀장님 및 팀원분과 충분한 의논 끝에 최종적으로는 Next (React)를 사용하기로 결정하였습니다!
”AI 검색” 프로젝트 뿐 아니라, 앞으로의 프로젝트에서 프론트엔드 기술은 React를 사용하기로 했어요.
그렇게 결정된 “AI 검색“ 프로젝트의 기술스택은 아래와 같습니다!
Framework : Next 15 (React 18, TypeScript)State Management : Zustand Data Fetching & Caching : React QueryTesting : Jest, Storybook
그럼, 이제 React 외에 기술들은 어떤 이유로 도입을 하게 되었는지 짧게 설명해볼게요.
제목없음
TypeScript
JavaScript에서는 개발자가 타입을 꼼꼼히 체크하지 않는 이상, 타입 안정성을 보장하기 어려운 경우가 많아요.런타임에서 발생할 수 있는 여러 문제를 TypeScript를 통해 빌드 타임에 미리 잡을 수 있고,잘 정리된 타입 시스템은 협업 시에도 큰 도움이 돼요.
안정성과 유지보수를 고려했을 때, TypeScript는 필수적이라고 판단하여 프로젝트에 도입하였습니다.
제목없음
Zustand
처음에는 상태 관리를 위해 Redux를 고민했었어요. “React의 상태 관리 라이브러리를 대표하는 게 뭐야?” 라고 물어본다면 Redux를 말할 수 있을 것 같아요. 많이 사용하는 만큼 큰 커뮤니티를 가지고 있고, 다양한 미들웨어와 DevTools 등 개발 도구를 지원해 주기도 해요.
그러나, “AI 검색“ 프로젝트에서 Redux를 도입할 것인가에 대해서는 아래와 같은 고민들이 생겼어요.
▪️설정이 복잡하다.▪️러닝 커브가 높다.▪️번들 크기가 크다.
“AI 검색“ 프로젝트는 규모가 크다거나, 복잡한 프로젝트는 아니었어요. 상태 관리는 대부분 Props 혹은 Context API로 해결이 가능한 수준이었고, 전역 상태나 공통 상태 관리를 할 데이터가 많지 않은 편이었어요. 그래서 “Redux가 AI 검색 프로젝트에 비해 너무 과한 것은 아닌가?”라는 고민이 되었죠.
✅ Zustand는 이러한 고민을 해결해 줄 대안이었습니다.
▪️설정이 간단하고, 보일러플레이트 코드가 거의 없어요.▪️Redux보다 훨씬 가벼운 번들 크기를 가지고 있어요.▪️직관적인 API 덕분에, 러닝 커브가 낮고 사용하기 편리해요.
Redux와 Zustand의 다운로드 수 비교 (5년)
Redux와 Zustand의 다운로드 수를 비교해보았을 때, 꾸준히 늘어나는 것을 볼 수 있어요!
여러 고민과 상황을 도입해본 결과, “AI 검색“ 프로젝트에는 Zustand가 더 적합하다고 판단하여 도입하게 되었습니다.
제목없음
React Query
React Query란 서버 상태 관리를 위한 라이브러리예요. 예를 들어 “디비피아는 어떤 서비스야?“ 라고 검색을 하게 되면 검색 과정이 진행되고, 서버에서 “디비피아는 연구를 돕는 똑똑한 학술콘텐츠 플랫폼이야!“라고 클라이언트(브라우저)로 응답이 올거에요. 서버에서 받은 값을 클라이언트에서 관리하는 도구라 생각하면 돼요.
React Query를 사용하면 서버에 요청을 하는 게 간단해지고, 응답 값을 캐싱하여 관리하거나 상황에 따라 데이터를 리패칭 하는 등 다양한 기능을 편하게 사용할 수 있어요. React Query에서 지원해주는 “캐싱” 기능은 핵심 기능 중 하나예요. “AI 검색“ 프로젝트에서도 많이 사용하고 있어요.
만약 일주일 전에 “오늘 날씨에 대해 알려줘“ 라고 질문하고 답변을 받은 상태라면 일주일이든 한 달이든 사용자가 다시 추가로 검색하기 전에는 데이터가 그대로 남아있어요. 즉, 기존에 검색 했던 답변들은 업데이트 주기가 일정하지 않다고 볼 수 있죠.
사용자가 추가로 검색을 이어서 하지 않는다면 답변 데이터는 계속 그대로인데, 굳이 해당 답변을 클릭할 때마다 API를 호출해야 할까요? 만약 그럴 경우 같은 데이터를 가져오기 위해 매번 HTTP 통신을 해야하고, 통신이 올 때까지 기다려야 해요. HTTP 통신이 많이 발생한다면 비용적인 측면에서도 좋지 않아요. UX와 리소스 관리 둘 다 해치는 요소가 되겠죠.
이런 문제를 React Query의 “캐싱“으로 해결 할 수 있어요. 기존에 저장된 데이터를 불러올 때 최초 한번만 HTTP 호출을 통해 데이터를 가져옵니다. 그리고 그 데이터는 React Query를 통해 캐싱 처리를 합니다. 그리고 다시 해당 데이터를 불러올 때는 캐시에 저장된 데이터를 불러와요. 그러다가 사용자가 해당 데이터를 업데이트하는 행동을 할 경우 캐시를 무효화하고 데이터를 업데이트합니다.
추가로, React Query는 dehydrate와 hydrate를 지원해주고 있어요! 덕분에 Server Component에서 HTTP 통신을 통해 데이터를 미리 가져온 후에 클라이언트에 전달할 수 있어요. 아래는 같이요!
만약 React Query를 사용하지 않고 이런 기능들을 직접 구현해야 한다면…. ㅎㅎ (생각하고 싶지 않네요 😢)
React Query를 사용하는 이유 간단 요약
✅ 간단한 코드로 API 호출 및 상태 관리 가능✅ 자동 캐싱 및 리패칭 (Refetching) 기능 지원✅ 백그라운드에서 데이터 동기화 및 최신 상태 유지✅ 비동기 데이터 요청을 더욱 쉽게 관리✅ Dehydrate, Hydrate 지원
프로젝트에 꼭 필요한 기능들이기에, React Query를 도입하였습니다.
제목없음
Testing - Jest
테스트 같은 경우, 단위 테스트를 Jest로 하고 있고, 공통 디자인 컴포넌트는 Storybook을 통해 관리하고 있어요.
Jest가 무엇인지 간단하게 설명해볼게요
위와 같은 Radio 컴포넌트를 프로젝트에서 사용한다고 가정해볼게요. 그러면 어떤 것을 테스트해봐야 할까요? 여러가지가 있겠죠. 버튼이 잘 클릭되는지, 클릭한 값이 잘 선택되었는지, Email, Phone 등 텍스트가 잘 출력되었는지 간단한 컴포넌트인데도 테스트를 해봐야 하는 것들이 적지 않아요. 상황을 한번 들어볼게요.
1. 라디오 버튼을 무사히 개발 완료했어요.2. QA를 거쳐 테스트를 모두 완료하고 배포를 했어요.3. 몇 달이 지났어요.4. 어? 갑자기 특정 상황일 때는 Radio 버튼을 클릭해도 클릭이 되지 않게 해달라는 요청이 들어왔어요!
🧑‍💻개발자 : (어떻게 개발했는지도 기억이 잘 안 나는데… 일단 기능만 추가하자!) 개발 완료했어요~ QA 서버에 올릴테니 테스트 부탁드립니다!
✔️QA팀 : 어? 그런데 이전에 잘 되던 기능이 안되는데요? 한번 확인해주세요~
🧑‍💻개발자 : (왜 저게 안되지??) 넵 확인해볼게요~
✔️QA팀 : 어? 이것도 안되네요~ QA 진행이 어려울 것 같아요. 기존 기능들까지 모두 전체적으로 검수 후 다시 요청해주세요!
🧑‍💻개발자 : ?!?!?!?!?!?!?!?!
(실제로 이런 경우는 거의 없어요! 예시로만 봐주세요.)
이런 경우 기존에 만들었던 기능을 모두 하나씩 다 확인해봐야 하는데, 시간이 많이 경과하여 기억이 잘 나지 않을 수도 있고, 또 다른 동료들이 코드를 수정한 경우가 있을 수도 있어 모든 히스토리를 확인하기가 쉽지 않습니다.
그렇지만 이럴 때 미리 작성해놓은 테스트 코드가 있을 경우, 위와 같은 상황에 훨씬 빠르게 대응할 수 있어요.아래는 Radio 컴포넌트의 간단한 테스트 코드입니다.
Radio 컴포넌트를 화면에 노출하고, onChange 이벤트가 발생했을 때 실행될 함수를 만들었어요.
그리고 “Option 3”의 radio 버튼을 변수에 담아서 클릭을 하고, mockOnChange 함수가 1번 호출된 게 맞는지,선택된 값이 “option3“ 값이 맞는지 확인해줍니다.
테스트 파일을 실행하면, 위와 같이 성공했는지, 혹은 실패했는지 여부와 시간이 얼마나 소요되었는지 확인할 수 있어요.만약 개발을 다 해놓고, 테스트 파일도 다 성공 케이스로 만들어 둔 후 나중에 코드를 변경하거나 기능을 추가할 때 테스트 파일을 실행해봄으로써 기존 코드가 문제가 있는지 없는지 확인해볼 수 있어요.
테스트 파일이 없을 경우 실제로 사용되는 화면을 접근해서 클릭해보고, 함수를 호출해봐야 하는 시간들을 테스트 파일을 작성해둠으로 시간을 절약하고 편의성을 챙길 수 있습니다!
단점으로는, 코드가 수정될 때 테스트 코드를 같이 수정해줘야 하고, 또 개발 일정이 여의치 않을 때는 테스트 코드까지 같이 작성하기가 쉽지 않다는 점이 있어요. 만약 자주 변경되는 컴포넌트나 기능들의 경우 오히려 테스트 코드를 작성하지 않는 게 더 관리가 편할 수도 있습니다. 그래서 상황에 맞춰서 하는 게 중요한 것 같아요.
현재는 Jset만 도입이 되어 있고, E2E 테스트(Cypress 등)도 추후 도입 예정입니다!
제목없음
Testing - Storybook
스토리북은 프론트엔드 동료 및 다른 팀원들과 협업을 위해 사용되고 있는 UI 컴포넌트 테스팅 도구예요!
버튼, 라디오, 인풋 영역, 알럿, 토스트, 토글… 등등 프론트엔드에서는 여러 곳에서 재사용 가능한 컴포넌트가 많아요. 저희는 이를 “UI 공통 컴포넌트”로 분리했어요. 아토믹 디자인 패턴까지는 아니지만 비슷하다고 볼 수 있을 거 같아요! 현재 구조는 “Container/Presentational“ 패턴으로 개발되어 있어요. “Presentational“ 컴포넌트는 UI 만을 위한 컴포넌트로, 비즈니스 로직이나 의존성에 영향을 받으면 안돼요.
예를 들어, 버튼 컴포넌트면 버튼을 클릭 했을 때를 감지해서 이벤트를 상위 컴포넌트로 전달하거나, 상위 컴포넌트에서 전달된 텍스트 등을 보여주기만 해야 해요. 상태를 관리하거나, 값을 수정하거나 하지 않아요.
비즈니스 로직은 “Container”에서 관리합니다. “Presentational“ 컴포넌트는 말그대로 보여주기만 하는 컴포넌트예요! 이들의 관리를 도와주는 도구가, “Storybook“ 입니다!
아래는 실제 사용하고 있는 스토리북의 “Button“ 컴포넌트예요.
모양은 간단해보이지만, 사용할 때 전달해야 하는 Props가 꽤 많아요. 어떤 컬러들을 사용할 수 있는지, 텍스트를 영어로 입력할 때, 한글로 입력할 때는 어떻게 나오는지, 클릭을 못하게 할건지 등 옵션들이 많은데, 이걸 개발할 때 하나씩 화면에 적용하고 눈으로 보고 하려면 시간이 꽤 오래 걸릴 거예요.
또한, 디자이너, 기획자 동료분들이 의도하신 대로 만들어졌는지 다른 동료들도 확인해봐야 하는데, 하나하나씩 서비스를 이용하면서 확인을 하는 것보다 어디에 모아두고 한번씩 클릭해보고 하면 더 편리하지 않을까요?
그런 편리함을 제공해주는 도구가 “Storybook“ 입니다! 폴더 구조로 각각의 형태에 맞게 보여줄 수도 있으며, 직접 옵션을 수정하거나 텍스트를 입력하는 등 수정도 가능해요. 서비스와 별개로 배포도 가능하기 때문에, 서비스를 오픈하기 전, 혹은 개발한 것을 배포하기 전에 “Storybook“에 미리 적용하고 배포함으로써 리뷰를 빠르게 진행할 수 있어요.
“AI 검색“ 프로젝트는 디자인 시스템이 적용되어 있기 때문에 “Storybook“은 꽤나 유용하고 사용되고 있고, 앞으로도 꾸준하고 활발하게 사용될 예정이에요!
제목없음
예외 케이스 처리
저는 개발할 때 “실패“에 대한 케이스 처리가 굉장히 중요하다고 생각해요. 생각한대로, 기획한대로, 개발한대로 사용자가 문제없이 사용했으면 너무 좋겠지만 언제나 예외는 발생하는 법이에요. 만약 HTTP 통신 후 응답이 오지 않는다면? 통신이 실패한다면? 타입이 보장되지 않으면? 통신이 너무 길어진다면? 사용자가 검색을 하다가 브라우저를 새로고침 한다면? 개발을 하다보면 이렇게 물음표가 계속 떠오를 때가 있어요.
사용자의 브라우저, 네트워크 상황, 사용 기기, 사용자의 데이터 등 예외가 발생할 수 있는 케이스는 너무나도 많고 예측이 어려워요. 예외 케이스 처리를 위해 저희는 오류 처리 프로세스를 총 2가지로 분류하여 관리하고 있어요.
1. 정의되지 않은 오류, 예측 불가능한 오류2. 정의된 오류, 예측 가능한 오류
1번 같은 경우 HTTP 통신 할 때 공통으로 사용하는 HTTP Fetcher에서 오류 발생 시 Error 객체를 throw 해요. 그리고 Zustand Error 스토어에 오류 값을 업데이트해요. 그러면 해당 값을 구독 중인 ErrorCatcher.tsx 컴포넌트에서 값을 감지하고 조건에 따라서 화면을 Reload 하거나, Redirect 해요. 이 때 사용자에게 별도의 알림을 줍니다.
2번 같은 경우 HTTP 통신 함수의 파라미터로 특정 값을 넘겨줘요. 그 값을 활성화하는 경우는 공통적으로 사용하는 1번 오류 처리 프로세스를 사용하지 않겠다는 의미예요. 이 경우에는 오류가 발생해도 화면을 Reload 하지 않거나, Redirect 하지 않고 현재 화면에서 무언가 처리가 필요할 경우 사용해요.
예를 들어 HTTP 통신이 실패했을 때 특정 화면을 보여준다거나, 알럿을 띄워준거나, 그럴 때 2번의 옵션을 사용해서 해당 화면만 예외처리를 따로 해주는거예요. ErrorBoundary 를 사용할 수도 있고, 직접 처리를 해줄 수도 있는거죠.
AI 모델을 사용하다 보니 정의되지 않은 오류나 예측 불가능한 오류가 조금은 있는 편이었어요. 그렇지만 1번 케이스에서 오류가 잡히기 때문에 사용자 입장에서 화면이 멈추거나, 동작이 이상하게 된다는 등의 문제들은 처리 할 수 있었어요. 그러나 사용자가 어떤 오류를 많이 겪고 있는지, 어떤 오류로 인해 서비스를 이탈하는지, 혹시 개발팀에서 예상하지 못하는 오류를 겪고 있는 건 아닐지 알기가 힘들어요. 사용자가 겪은 오류를 확인하는 건 서비스 운영에 굉장히 중요한 데이터예요.
그런 오류들을 모니터링 하기 위해 저희는 Sentry라는 모니터링 도구를 도입했어요. 만약 사용자가 서비스를 이용하다가 오류를 겪으면 해당 오류를 Capture 해서 Sentry에 전달하고, Sentry에서 지원하는 화면을 통해 오류를 확인해볼 수 있어요.
Sentry 오류 발생 1
Sentry 오류 발생 2
오류가 발생한 시점의 사용자가 사용한 브라우저나, 기기를 확인할 수 있고 오류를 보다 상세하게 확인 할 수 있어요. 또 어느정도의 치명적 오류인지 구분해서 Slack으로 알람을 보낸다거나 하는 처리가 가능해요. 이를 통해 오류를 줄여나가면서, 서비스를 더욱 안정적으로 만들어나가고 있어요.
Sentry는 무료 플랜과 유료 플랜이 있는데, 누리미디어에서는 이런 지원을 아끼지 않기 때문에 사용할 이유가 충분하다면 사용이 가능합니다.
제목없음
결과
그렇게 “AI 검색“ 서비스가 오픈되었습니다!
(👉 AI 검색 서비스 바로가기)
아직 개선해야 할 것도 꽤 많고, 기능적으로 더 업데이트 될 내용도 많아요! 서비스 배포까지 앞만 보고 빠르게 달려왔기에, 앞으로 챙겨야 할 게 꽤 많습니다. 코드 리뷰 도입, 성능 최적화, 배포 프로세스 정립 등등..
빠른 시간에 런칭한 서비스인 만큼 오는 길이 쉽지는 않았지만, 그만큼 앞으로 해야 할 것은 많고 더욱 높이 오를 일만 남았다고 생각합니다. 앞으로도 디비피아에서 서비스할 다양한 AI 서비스를 기대해주세요!