Kredis(Keyed Redis)는 최근 Rails 개발자 도구 모음에 합류한 라이브러리입니다. Redis에 구조화된 데이터를 저장하고 조회하는 과정을 크게 단순화해 주는 것이 목표입니다.
이 두 편으로 구성된 시리즈의 첫 번째 글에서는 먼저 Kredis의 동작 원리를 살펴보겠습니다. 이어서 맞춤형 Redis 키를 활용해 일시적인 UI 상태를 저장하는 실전 예제를 진행해 보겠습니다.
그럼 바로 시작해 보겠습니다!
Rails를 위한 Kredis 소개
Kredis는 세 가지 방식으로 개발 편의성을 높여 주는 래퍼(wrapper)를 제공하는 Railtie입니다.
- 루비다운 API: 예를 들어
Kredis.list나Kredis.set같은 컬렉션 타입은 네이티브 Ruby 타입과 해당 API를 최대한 그대로 재현합니다. - 타입 지정(Typings): 특히 컬렉션에서 유용합니다. Kredis는 요소들을
datetime,json같은 표준 데이터 타입으로(혹은 그 반대로) 타입 캐스팅까지 알아서 처리해 줍니다. - ActiveRecord DSL: 아마도 이 라이브러리의 가장 큰 장점일 것입니다. 어떤 Redis 데이터 구조든 특정 모델 인스턴스와 손쉽게 연결할 수 있습니다.
README에 소개된 예제는 다음과 같습니다.
Kredis의 핵심 가치는 특정 레코드에 연관된 일시적인 정보를 세션과 무관하게 저장할 수 있다는 점입니다. Rails에서 데이터를 영속화해야 할 때 일반적으로 선택할 수 있는 대표적인 두 가지 방법은 다음과 같습니다.
- ActiveRecord: 대부분의 경우 컬럼을 추가하거나 데이터 모델을 수정해야 합니다. 마이그레이션이 필요하고, 경우에 따라 기존 레코드에 대한 백필(backfill) 작업도 따라옵니다.
- Session: 모든 Rails 앱의 기본 키/값 저장소로, 설정이 거의 필요 없다는 장점이 있습니다. 다만 여기에 저장된 데이터는 로그인/로그아웃 주기를 넘어 유지되지 않습니다.
Kredis는 여기에 세 번째 선택지를 제시합니다. 모델에서 DSL을 호출하는 것 외에는 별도의 설정이 거의 필요 없지만, Redis 인스턴스가 다운되지 않는 한 데이터는 세션을 넘어, 심지어 디바이스를 넘어서도 유지됩니다. 따라서 웹 앱과 함께 출시되는 모바일 앱처럼, 여러 디바이스에 걸쳐 공유하고 싶은 중요도가 낮은 정보를 저장하는 데 Kredis가 제격입니다.
사례 연구: Kredis로 UI의 접힘/펼침 상태 저장 및 복원하기
Kredis의 좋은 활용 사례 중 하나는 다음과 같은 UI 상태를 영속화하는 것입니다.
- 사이드바 열림/닫힘 상태
- 트리 뷰 노드 열림/닫힘 상태
- 아코디언 접힘/펼침 상태
- 커스텀 대시보드 레이아웃
- 데이터 테이블에 표시할 행 수
이번 글에서는 예시로 <details> 요소의 접힘/펼침 상태를 관리하는 방법을 살펴보겠습니다.
먼저 새 Rails 앱을 만들고, Gemfile에 kredis를 추가한 뒤 설치 스크립트를 실행합니다.
참고: 이 명령은 config/redis/shared.yml 위치에 Redis 설정 파일을 생성합니다.
이 글의 나머지 부분에서는 로컬에서 실행 중인 Redis 인스턴스가 있다고 가정합니다. macOS에서 Homebrew를 사용한다면 다음 명령 한 줄이면 충분합니다.
각 운영체제에 맞는 설치 방법은 공식 "Getting Started" 가이드를 참고하세요.
사용자 인증 준비
UI 상태 정보를 담을 엔터티로 User 모델을 사용하겠습니다. 불필요한 논쟁을 피하기 위해 Devise가 기본으로 제공하는 구성을 그대로 활용하겠습니다.
이후 Rails 콘솔에서 테스트용 사용자를 하나 생성합니다.
예제 앱: 온라인 백화점
Kredis가 복잡한 트리 구조의 상태를 얼마나 잘 영속화하는지 보여 주기 위해, 우리가 온라인 백화점을 운영한다고 가정해 보겠습니다. 이를 위해 Department(부서/카테고리)와 Product(상품) 모델을 스캐폴드로 생성하고, 카테고리 간 셀프 조인(self join)을 통해 두 단계 깊이의 중첩 구조를 만들겠습니다.
물론 트리 구조의 루트 노드를 허용하려면 부모 필드가 null일 수 있도록 허용해야 합니다.
Department와 Product 모델은 다음과 같이 정의됩니다.
마지막으로 faker를 사용해 시드(seed) 데이터를 생성합니다.
상점 프론트엔드 스캐폴딩
상점의 메인 화면 역할을 할 아주 간단한 HomeController를 만들어 보겠습니다.
여기서는 카테고리의 하위 항목에 대한 셀프 조인을 수행하여, 실제로 하위 카테고리를 가진 노드, 즉 트리의 루트들만 조회합니다.
인덱스 뷰에서는 카테고리를 두 단계 깊이의 <details> 요소로 감싼 중첩 트리 뷰를 구성합니다.
이제 의도적으로 우스꽝스러운 상품 이름을 가진 카테고리 트리 뷰가 완성되었으며, 노드를 열고 닫으면서 탐색할 수 있습니다.

다음으로, 각 카테고리의 펼침 상태(disclosure state)를 영속화하는 작업을 진행하겠습니다.
카테고리 UI 상태를 Kredis에 저장하기
우리가 단계별로 수행할 작업은 다음과 같습니다.
User모델에open_department_ids라는 이름의kredis_set을 추가합니다. 집합(set)은 중복을 허용하지 않으므로, 카테고리 ID를 안전하게 추가하고 제거할 수 있습니다.- 다음 파라미터를 전달받는
UIStateController를 생성합니다.department_id: 해당 카테고리의 IDopen: 해당 카테고리의 열림 상태
kredis_set에 해당 카테고리를 추가하거나 제거합니다. <details>요소의 toggle 이벤트를 감지해 위 페이로드를 전송하는 Stimulus 컨트롤러를 작성합니다.
이제 하나씩 살펴보겠습니다!
User 모델에 Kredis 데이터 구조를 추가하는 것은 kredis_set을 호출하고 식별자를 넘겨 주는 것만큼 간단합니다.
다음으로 UI 상태 업데이트를 전달받을 UIStateController를 생성합니다. 이때 생성된 라우트를 patch 엔드포인트로 설정해야 합니다.
Kredis API와의 첫 만남은 컨트롤러에서 이루어집니다. Kredis는 Ruby 개발자의 직관을 최대한 존중하도록 설계되었기 때문에, << 연산자로 집합에 요소를 추가하고 remove 메서드로 삭제할 수 있습니다.
여기서 일어나는 일은 클라이언트가 전달한 open 파라미터 값에 따라 집합 내 특정 department_id의 존재 여부를 토글하는 것입니다. 전체 그림을 완성하려면 이 UI 상태 변경을 서버로 전송하는 클라이언트 측 코드도 작성해야 합니다.
요청 처리에는 @rails/request.js를 사용할 것이므로, 먼저 이를 pin 해야 합니다.
새로 만들 Stimulus 컨트롤러는 특정 <details> 요소에 부착되며, 카테고리 ID와 해당 요소의 open 상태를 FormData 객체에 담아 전송합니다.
이제 뷰 코드를 수정하여 각 <details> 요소의 toggle 이벤트를 감지하고, 이를 통해 UI 상태 업데이트를 트리거합니다.
DOM 상태 수동으로 복원하기
전체 흐름을 완성하는 데 마지막으로 남은 조각은, 사용자가 페이지를 새로 고친 뒤 DOM을 원래 상태로 복원(rehydrate)하는 것입니다. 이는 해당 카테고리 ID가 Kredis 집합에 존재하는 경우 <details> 노드에 open 속성을 추가하는 방식으로 수동 구현할 수 있습니다.
마지막으로 결과를 확인해 보겠습니다. 개별 트리 노드의 열림/닫힘 상태가 두 단계 깊이에 걸쳐 모두 유지되는 것을 볼 수 있습니다.
다음 편 예고: 범용적인 사용자 로컬 UI 상태 컨테이너
이 두 편짜리 시리즈의 첫 번째 글에서는 Kredis를 소개하고, Kredis를 활용해 UI의 접힘/펼침 상태를 저장하고 복원하는 방법을 살펴보았습니다.
온라인 백화점 예제를 통해 Kredis가 복잡한 트리 구조의 상태를 어떻게 영속화하는지 확인했고, 마지막에는 DOM을 수동으로 복원했습니다.
하지만 이 방식은 상태 종류가 늘어날 때마다 수많은 Kredis 키를 새로 만들어야 한다는 부담이 있습니다. 다음 편에서는 범용적인 사용자 로컬 UI 상태 컨테이너를 통해 이 문제를 어떻게 해결할 수 있는지 자세히 알아보겠습니다.
그때까지 즐거운 코딩 되세요!
P.S. Ruby Magic의 글이 발행되는 대로 바로 읽고 싶다면 Ruby Magic 뉴스레터를 구독하세요. 어떤 글도 놓치지 않을 수 있습니다!