Hema 프로젝트를 하며

Hema 프로젝트

Hema 프로젝트는 헬스케어 창업 동아리 Medilux에서 진행했던 “경도 인지 장애 완화를 위한 인지 재활 챗봇”이다. 한 학기동안 팀장으로써 Hema라는 서비스를 만들었고 베타 버전까지 개발 완료하여 배포하였다.

왜 시작했는가

가장 처음 들고 나갔던 아이디어는 카카오톡 챗봇으로 사용자가 몸에 증상이 있으면 바로바로 쉽게 기록할 수 있게 하는 서비스였다. 챗봇이라는 플랫폼에 집중해서 아이디어를 어필했었다. 접근성의 측면에서 봤을 때 스토어에 들어가서 설치해야하는 어플리케이션은 사용자에게 진입 장벽이 높다고 생각했다.

카카오톡 챗봇의 경우는 이미 거의 모든 국민들의 스마트폰에 설치되어있고 대부분 알림을 끄지 않는다. 기본적으로 깔려 있는 거대한 플랫폼 위에서, 매일 보는 익숙한 UI에서 작동하는 서비스는 원하는 가치를 전달하기 좋은 플랫폼이라고 생각되었다.

그렇지 않은 팀원도 있지만 함께 하게 된 대부분의 팀원은 이런 점에서 내 아이디어를 선택했다고 해주었다. 물론 증상 기록 서비스는 사람들이 서비스를 쓸 요인, BM을 붙이기 힘들다는 이유로 고민 끝에 다른 아이디어로 변경하기로 했다.

경도 인지 장애를 선택한 이유

모두가 두려워하고 걱정되는 건강 문제인 치매가 회의 끝에 주제로 선정되었다. 하지만 치매(알츠하이머)라는 질병 특성상 애매한 부분이 많았다. 치매는 치료, 예방, 완화의 개념이 완벽히 분리되지 않는다. 이미 치매가 걸렸다면 완치를 기대하기는 현재로써는 거의 불가능하다. 따라서 약물 치료나 인지 재활 치료를 병행하는데 이 또한 증상을 완화하는 정도에 그친다.

우리가 치매의 증상을 성공적으로 완화하는 서비스를 만들기는 힘들다고 판단했고 설령 만든다 한들 이미 중증 치매 환자들은 자의로 서비스를 사용하기 힘들거나 입원한 상태에서 최적의 치료를 받고 있다.

따라서 우리는 치매의 전 단계인 경도 인지 장애에 집중했다. 이 단계는 치매의 전 단계라고 할 수 있으며 이때 적절한 치료를 진행하면 치매로 진행되는 기간을 상당히 늦출 수 있고, 일부는 치매로 진행되지 않게도 할 수 있다. 이 때의 치료 또한 약물과 같은 무거운 치료가 아닌 인지 자극같이 가벼운 활동만으로도 많은 효과를 볼 수 있다.

챗봇과 경도인지장애의 결합

우리의 타겟층인 경도인지장애 환자들은 대부분 50대 이상의 중장년층이다. 이 나이대에 대해 생각했을 때 “모바일 앱 설치에 대해 어려움을 겪지 않을까?”라는 가설을 세웠다. 부모님 세대를 관찰했을 때, 새로운 앱을 까는 것에 대해 귀찮아하고 사용시간이 이미 깔려있는 기존 앱에 고착화되어있는 것을 볼 수 있었다. 또한 여러 치매 관련 앱을 조사한 결과 처음 사용하는 사람에게는 UI가 익숙하지 않아 진입장벽이 높다고 생각했다. 따라서 처음 주제로 생각했던 카카오톡 챗봇에 대해 고려하기 시작했다.

카카오톡 챗봇의 장점은 아래와 같다.

  • 이미 대부분의 사용자에게 설치되어있다(98.9%)
  • 익숙한 사용 방법과 UI를 가지고 있다.
  • 공유, 알림, 채널 홈 등 유용한 기능이 이미 구현되어있다.

카카오톡 챗봇으로 잘 구현만 된다면 충분히 검증 해 볼만한 아이템이라고 생각했다.

만난 문제점들

  1. 사용자 인터뷰 서비스의 사용 대상인 경도 인지 장애 환자들을 만나기가 힘들었다. 대상자들이 모여있는 그룹인 치매 센터와 복지센터에 가장 먼저 연락해보았다. 하지만 대부분 센터의 경우 연락처를 찾기부터 힘들었고 많은 곳에 메일을 넣었지만 답장이 없는 곳이 대부분이었다. 연락에 성공한 곳도 연구의 목적이 아니면 방문해서 이야기를 나누는 것 조차 거부되었다. 그래서 차선책으로 설문지를 만들어 우리 팀원들의 부모님세대, 조부모님 세대에 대해 설문을 했지만 직접 만나서 인터뷰하고 스마트폰 사용 경험을 관찰하기는 힘들었다.
  2. 대화형식의 한계 카카오톡 챗봇은 사용성과 접근성은 높지만 대화 형식이라는 고정 적인 포맷이 존재한다. 그 포맷 안에 우리의 가치를 녹여내는 것이 가장 큰 숙제였다. 대화 포맷은 시계열 데이터가 축적되는 방식이고 우리가 원하는 플로우대로 사용자가 쓸 수 있게 하려면 상태 관리에 많은 공을 들여야 했다.
  3. 경도인지장애에 대한 인식 경도인지장애라는 개념을 모르는 사람이 국민의 48%를 차지한다. 단어를 알더라도 이 시기가 치매 지연 및 치매 예방을 위한 중요한 시기인지 모르는 사람이 73%였다. 우리의 서비스를 홍보하고 사용하게 만드려면 우리가 해결하고자 하는 문제점을 인식시켜야 한다는 점이 큰 문제였다. 그래서 우선 Hema 인스타 채널을 만들어 의대 팀원, 기획자 팀원과 함께 짧은 칼럼을 써서 게시했다.

개발 포인트

대화 저장
서비스 특성 상 대화 플로우 자체가 여러 성격으로 나누어졌다. 가장 먼저 튜토리얼이 있었고, 그 후 사용자 정보 설정, 문제 제출, 자유 대화 등이 있었다. 이 정보들을 저장하기 위해 “Session”이라는 테이블로 대화 플로우를 정의했다.
기획에서 의도한 플로우의 step들의 타입을 Enum으로 정의하여 session type으로 사용했고 해당 세션에 관련 대화들이 달리는 형식으로 구성했다. 이렇게 구성하니 사용자가 튜토리얼을 언제 완료했는지, 설정을 언제하는지, 태스크를 다 푸는데 얼마의 시간이 걸리는지 파악하기 쉬워졌다.

인지 강화 문제 생성
먼저 문제는 인지 강화 문제집을 보고 유사한 문제를 제작하여 사용하였다. 이 문제들의 경우 형식이 고정되어있었다면 문제 생성이 편했겠지만 거의 모든 문제들의 형식이 달랐다. 이미지를 보고 푸는 문제, 현재 시간을 맞추는 문제, 화투 그림을 보고 점수를 계산하는 문제 등이였다. 이런 형식들을 정규화하여 코드를 만들 수 없었기 때문에 각 문제 당 생성 함수를 따로 만드는 방식을 선택하였다. 이렇게 할 경우 문제가 늘어날수록 코드 자체를 고쳐야한다는 문제가 생겼다.
따라서 팀원들이 사용할 수 있는 BaseTask abstract 클래스를 사용하였다.

class BaseTask(ABC):
    """
    Task에 대한 기본 인터페이스를 정의하는 추상 클래스
    모든 Task 클래스는 이 클래스를 상속받아 구현해야 함
    """
    
    def __init__(self, stage: int = 0, user: models.User = None):
        self.stage = stage
        self.max_stage = self._get_max_stage()
        self.user = user
        
    @abstractmethod
    def _get_max_stage(self) -> int:
        """최대 stage 수를 반환"""
        pass
    
    @abstractmethod
    def get_story(self) -> str:
        """Task에 맞는 스토리를 반환"""
        pass
    
    @abstractmethod
    def create_question(self) -> Tuple[str, Optional[models.TaskImage]]:
        """
        Task에 맞는 질문을 생성하고 반환
        return (문제 내용, 이미지 - 이미지 없으면 None)
        """
        pass
        
    @abstractmethod
    def create_answer(self, answer: str) -> Tuple[Dict[str, str], Dict[str, str], Dict[str, str]]:
        """
        Task에 맞는 답변을 생성하고, 반환
        정답 하나, 오답 두개
        각각에 맞는 반응도 생성
        return (
            {
                "content": 정답 내용,
                "reaction": 정답 반응
            },
            {
                "content": 오답1 내용,
                "reaction": 오답1 반응
            },...
        )
        """
        pass
    
    def is_max_stage(self) -> bool:
        """현재 stage가 최대인지 확인"""
        return self.stage >= self.max_stage
    
    def get_todo(self) -> str:
        """Task에 맞는 할일을 반환"""
        pass

해당 BaseTask 클래스를 만들고 이 클래스를 상속받아 각기 다른 유형의 문제를 생성하는 클래스를 만들 수 있도록 알려주었다. 그리고 문제를 실제 생성할 때는 태스크 클래스가 들어있는 패키지 안의 모듈들을 불러오는 코드로 자동으로 연결될 수 있게 했다.

def get_task_class_instance(self, user):
    """캐시된 클래스 정보를 사용하여 인스턴스 반환"""
    global _task_class_cache
    
    if not _task_class_cache:
        import importlib
        import pkgutil
        import chatbot.tasks.task_class as task_class_package
        import chatbot.tasks.image_task_class as image_task_class_package

        # task_class_package와 image_task_class_package 모두 처리
        for package in [task_class_package, image_task_class_package]:
            for _, module_name, _ in pkgutil.iter_modules(package.__path__):
                module = importlib.import_module(f"{package.__name__}.{module_name}")
                # 모듈의 모든 속성을 확인
                for attr_name in dir(module):
                    attr = getattr(module, attr_name)
                    # 실제 클래스인지 확인
                    if isinstance(attr, type):
                        _task_class_cache[attr_name] = attr
    
    # 캐시에서 클래스 찾기
    if self.class_name in _task_class_cache:
        return _task_class_cache[self.class_name](task_category=self, user=user)
    
    raise ValueError(f"클래스를 찾을 수 없습니다: {self.class_name}")

매번 task 클래스들을 만들면 자원 낭비가 생기기 때문에 처음 접근할 때 task 목록을 메모리에 캐싱되게 하여 효율을 높였다.

이번 프로젝트를 진행하며 느낀점

| 나 스스로가 사용자가 아닌 서비스는 유저 인터뷰에 많은 공을 들여야한다.
내가 사용자가 아니라면 그 서비스를 만드는데 있어서 몇 배는 더 많은 고민이 들어간다. 서비스의 기획단계에서부터 이걸 사람들이 쓸까? 라는 궁금증이 자연스레 생길 수 밖에 없다. 기획이 진행되면서도 이 부분이 진정 필요한 부분인지에 대해 잦은 고민이 생길 것이다. 그때마다 실제 사용자의 피드백이 절실히 필요하다. 하지만 이번 프로젝트와 같이 실 사용자와 우리의 거리가 먼 경우는 그것이 너무 어려웠다.