카카오테크 부트캠프 풀스택 2기 해커톤을 진행하고 작성한 후기입니다.

제가 팀으로 참여한 밥팟팀은 본상 (2등, 카카오 대표이사상)을 획득했습니다.

밤샘으로 떡진 머리와 눈이 반쯤 감긴 처참한 모습 + 온라인으로 함께한 Kane도 있어요!

 

2박 3일간 진행했던 짧은 프로젝트였지만 배우고 깨달은 점이 많아서 꼭 후기를 남기고 싶었어요.

해커톤의 각 과정에서 경험했던 것들을 시간 순서대로 작성해 볼게요.

해커톤 형식과 주제

우선, 저희 카카오테크 부트캠프에는 3개의 트랙이 있어요. 풀스택, 인공지능, 클라우드 과정이 같은 공간에서 서로 각자의 트랙을 공부하는 형식이에요. 그래서 이번 해커톤은 3개의 트랙에서 최소 1명씩 구성되어 6명이 구성되는 팀 형식이었어요. 인공지능 트랙의 학생들이 있다 보니 이번 해커톤의 주제는 “LLM을 활용한 서비스”였습니다. 다양한 트랙의 학생들이 전문성을 가지고 진행하다보니 대학교 혹은 토이 프로젝트보다는 완성도 있는 프로젝트를 완성할 수 있었어요. 기존의 프로젝트와는 무엇이 달랐는지는 이후에 이야기해 볼게요.

팀 구성은 해커톤 이전에 아이디어톤이라는 행사로 팀 주제를 발표하는 자리가 있었어요. 아이디어톤에서 발표하는 주제들의 팀에 합류해서 해커톤을 진행하는 방식이었어요.

팀 모집

저는 아이디어톤 전에 좋은 기회로 인공지능 과정의 noah(이하 노아)와 같이 ‘밥팟’이라는 주제로 팀이 미리 결성했어요. 노아는 원활한 소통을 위해 오프라인 교육장에서 학습하는 인원들을 토대로 팀을 구성하려고 했고, 풀스택에서는 매일 출몰하는 저와 같이 팀이 되고 싶어 하는 것 같았어요. 저 이외에도 클라우드 과정의 yuna(이하 유나), 인공지능 과정의 joy (이하 조이)가 합류했어요. 

이후, 풀스택 과정의 팀원을 섭외하는 과정은 노아가 아닌 제가 하였어요. 아무래도 웹 프로젝트 과정에 경험이 있는 제가 적절한 팀원들을 섭외하기에는 좋아보였어요. 결국, 백엔드에서는 mumu (이하 무무, 아무무의 그 무무 맞습니다), kane (이하 케인)이 합류했어요. 무무는 자바 기반의 웹 서버를 제작해 주었고, 케인은 파이썬 기반의 AI 서버를 구축해 주었어요.

결국 풀스택 3(프론트엔드1, 백엔드2), 인공지능 2, 클라우드 1명의 구성으로 팀 구성을 완료했어요.

밥팟이란?

저희팀은 ‘밥팟’이라는 서비스를 제작하였는데 말 그대로 밥 파티라는 뜻으로, 원하는 식당을 토대로 밥 파티를 생성하고, 이에 신청해서 같이 식사하는 자리를 마련하는 커뮤니티입니다.

카테부 (카카오테크 부트캠프) 학생들이 식사 시간에 매일 같은 사람들과 밥을 먹는 것이 아니라, 다양한 사람들과 만나서 라포를 형성하고 원하는 음식도 먹을 수 있는 서비스를 제작해서 식사시간이 즐거워졌으면 하는 작은 바램으로 시작되었어요.

 

또한, 보통 식당을 고르는 경우 뭐 먹을까?로 시작해서 그럼 어디로 갈까?로 이어지는 것을 밥팟팀은 부정적 사건의 흐름으로 바라보았어요. 음식을 고르는 시간과, 식당을 검색하는 시간이 귀한 점심시간을 허비하는 과정으로 생각했던 거에요.

따라서, 밥팟팀은 자체 제작 LLM 챗봇인 밥봇을 도입하여 원하는 음식을 토대로 음식점을 추천해주는 기능을 제공해주기로 하였어요.
크롤링을 통해 카테부 주변의 식당 데이터를 수집하고, 밥봇이 사용자와의 대화를 통해 음식점을 추천해주는 기능이에요.

 

(AI팀 자랑)

원하는 음식, 식당의 분위기 등 다양한 입력값에도 올바른 선택지를 제공하도록 AI팀은 짧은 해커톤 기간에도 최적화에 상당히 많이 애써주셨어요. 특히, 밥봇은 외부 AI API 사용이 아닌 개발된 자체 모델이었다는 점이 기술적으로 대단한 성과 및 도전이었다고 생각해요.

 

밥팟 생성 과정 (좌), 생성된 밥팟 (우)

 

해커톤 프로세스

밥팟팀이 좋은 결과를 만들 수 있었던 이유는 짧은 해커톤 기간이었음에도 불구하고, 규칙적이고 체계적인 개발 과정을 거쳐서 프로젝트를 완성했기 때문이라고 생각해요. 저희는 아래와 같은 방법들을 사용했어요.

  1. 2~3시간 간격으로 스크럼을 통해 각자 진행하고 있는 프로세스를 공유한다.
    막히고 있는 지점이 있으면 이를 공유하고, 함께 해결 방법을 모색한다.
  2. 회의는 필요한 내용만 전달하며, 최대한 짧게 진행한다.
  3. 의사표시와 이해하지 못 하는 내용에는 확실히 표현하여 의사소통 오류가 없도록 한다.

가장 중요하게 작용했던 것은 1번의 주기적인 회의였다고 생각해요. 풀스택의 강사인 Kevin (항상 고마워요 케빈)의 조언을 토대로, 주기적으로 회의를 해서 각자 어디를 진행하고 있는지 공유하고 인원이 더 필요한 부분에는 여유로운 팀원이 붙어서 함께하는 방향으로 진행했어요. 이 과정에서 1명 밖에 없었던 클라우드 과정의 배포 부분을 백엔드의 무무가 함께한다던가, AI 서버 구축 과정에 인공지능 역할의 노아와 백엔드 역할의 케인의 역할 분담을 확실히 하여 각자의 분야에 전문성을 가지고 갈 수 있도록 하는 과정이 매우 효과적인 전략으로 작용했어요.

 

또한, 회의를 짧게 하는 것도 매우 좋은 방법이었어요. 이전 프로젝트에서 회의가 길어지면 팀원들의 집중력도 저하되고 회의가 끝나면 힘이 쫙 빠져서 개발을 못 하는 상황들을 여러 번 경험한 적이 있어요. 그래서, 이번에는 회의를 진행할 때 팀장이(It's me) 주도적으로 진행하고, 답변이 필요한 부분만 팀원들에게 대답을 요청하고, 깔끔하게 마무리 정리해서 모호함 없이 쟁점을 매듭짓는 방법으로 회의를 진행했어요. 그렇다고 해서 팀원들의 의견을 무시하고 팀장의 주관대로 진행하는 것이 아닌, 각자가 전문성을 가지는 부분들은 각 파트가 책임감을 가지고 해결하고, 협업을 하는 부분들은 서비스 확장을 고려해서 미래지향적인 부분으로 결정을하였어요. 특히, 각자 편한 부분으로 협업하는게 아닌, 서비스 자체의 확장성에 중점을 두고 개발했어요.

(팀원들은 제가 팀장 경험이 있어서 회의 진행에 경험이 있다고 생각했겠지만, 이전 프로젝트 회의에서는 모두가 함께 진행하는 방식을 사용했었고, 주도적으로 진행하는 건 이번이 처음이어서 조금은 떨렸습니다..)

 

이를 토대로 밥팟팀은 발표 5시간 전인 새벽 5시에 MVP의 모든 기능을 완성하고 발표 준비를 할 수 있었어요. 발표 준비 PPT는 유나가 도맡아 해주시고, 조이가 기깔나게 발표를 해주셨어요.

 

해커톤을 하며 느낀 감정

저는 사실 지금까지 해커톤을 별로 좋아하지 않았어요. 짧은 기간 동안 만든 프로젝트가 완성도가 좋을 수 없다고 생각했고, 만들고 버려지는 프로젝트는 더더욱 만들 필요성을 느끼지 못했어요.

 

이랬던 제가 밥팟 서비스를 제작하면서 많은 심경의 변화를 경험했어요.

 

저희 서비스 밥팟은 카테부 학생들을 위한 프로젝트였던 만큼 이후에도 계속 유지 보수할 생각이었어요. 그래서, MVP를 개발할 때도 최대한 미래지향적인 방향으로 기획을 진행했고 개발하였어요. 그렇다 보니, 자연스럽게 유지보수가 가능한 코드를 작성하게끔 되었고, DB 설계, 기획 등 다양한 부분에서 단기적 성향을 지닌 프로젝트로 개발되지 않았어요.

 

예를 들어, 밥팟팀은 (우수한 팀원들이 있어서) 생각보다 빠르게 개발이 진행되었다 보니 해커톤 기간 동안 계속해서 기능을 확장해 나갔어요. 특히, 초기 MVP 설계에는 밥팟팀의 추천 장소와 같은 기능(아래 사진 참고)은 존재하지 않았어요. 개발 과정에서 시간이 남아서 기능을 추가하기로 하였고, 음식점 광고를 표시하면 수익성을 창출할 수 있는 추천 장소 파트를 만들기로 기획했어요. 이 과정에서 음식점 DB의 속성에 ₩isAdvertisement₩ 과 같은 임시 필드를 만들어서 사용할 수 있었지만, 추후 광고를 제안받고 이를 관리하기 위해서는 ₩Advertisement₩ 테이블을 생성해야 함을 인지한 상태로 개발을 진행했어요.

 

광고 테이블이 필요한 값 (대략)

  • 광고 식당 (PK)
  • 광고 기간
  • 광고 우선순위

이렇게 앞으로 구현할 때 필요한 점들을 생각 해가며 기능을 구축했고, 각자의 파트가 있음에도 모두가 PM 역할을 함께 수행하며 기획과 설계 과정에 참여해서 프로젝트의 미래지향적인 개발을 꾸준히 생각했던 것 같아요.

 

메인화면의 밥팟팀의 추천 장소 공간

 

이런 경험을 하면서 해커톤 프로젝트 당연히 유지 보수하지 않을 거라는 확신을 가지고 임했던 과거 저의 마음가짐 자체가 잘못되었었음을 깨달았어요.

 

현업 수준의 프로젝트 아키텍처

또한, 기존의 프로젝트들은 프론트엔드 + 백엔드 + (인공지능 모델 API)의 간단한 아키텍처로 진행했었는데, 클라우드 팀이 합류하면서 프로젝트가 매우 완성도 있게 구축되는 것을 경험했어요. 특히, Vercel에 간단하게 배포했던 과거의 프로젝트들과는 달리, 도커와 EC2와 같은 클라우드 환경에 배포하는 것이 현업에서 실제 서비스를 개발하는 과정처럼 느껴졌어요. CI/CD는 Vercel이 제공해 주는 기능으로 받아들였었는데, Github Actions를 사용하며 현업에 가까운 더 전문화된 개발 과정을 느끼게 되었던 것 같아요. (또한, 사이트와 서버에 TLS를 적용하는게 까다로운 작업임을 이번에 몸소 느겼습니다.)

 

특히, 저희 모두 모든 파트에 전문성을 지니고 있지 않았어요. 저 같은 경우, 풀스택 과정과 설계에는 자신이 있었지만, 인공지능과 클라우드 과정은 기본적 개념만 알고 있을 뿐 자세한 지식과 구현 과정은 알고있지 못했어요. 따라서, 팀원 모두가 각자의 개발 프로세스를 소개하고 설명할 때 추상화하는데 굉장히 신경써주시는게 느껴졌어요. 이렇게 다양한 직군과 협업하는 경험을 또 언제 해볼까 싶은 생각도 들었습니다.

 

앞으로의 포부

밥팟 서비스는 앞으로 카테부 학생들이 유용하게 사용할 수 있을 만큼 성장할 예정이에요.

특히, 최종 발표를 진행하며 카카오 심사위원분(이름은 공개해도 되는지 모르겠어서..) 피드백을 남겨주신 점들을 반영해보고 싶어요.

  • 이용자들이 밥팟을 생성할때 궁금해할 정보 수집
    • 식당까지의 이동 거리
    • 웨이팅 시간
    • 식사 나오는 데 걸리는 시간
    • 식사하는 데 걸리는 시간
  • 판교 직장인들을 위한  커피챗 모집 탭으로 비즈니스 확장
  • 이용자들끼리 소통할 수 있는 환경을 더 만들어주기
    • 각 밥팟의 채팅방 혹은 공지
    • 좋아요와 스크랩 등 관심 있는 밥팟 등록
    • 인스타그램처럼 밥팟 사진을 공유할 수 있는 스토리 보드
  • 1/N 결제 기능
  • 밥팟 생성자에게 신청 알림 제공 (PWA)

 

이렇게 카테부에서의 첫 협업 프로젝트는 매우 성공적으로 끝낼 수 있었어요. 비록 대상(1등)을 받지는 못했지만, 프로젝트 과정에 대한 후회는 없어서 아쉽다고 느껴지지는 않는 것 같습니다.

 

두서없는 긴 글 읽어주셔서 감사하며, 카테부에서 들었던 가장 좋은 글귀로 글을 마무리하겠습니다.

"팀원을 도와준다는 것은 내 일이 아님을 인정한다는 것을 의미해요. 도움이 주는 것이 아닌 모두가 함께 해결하도록 해요. (구름 대표님)"

 

상품은 애플 매직 마우스!

'끄적끄적 > 회고' 카테고리의 다른 글

2024년 회고  (0) 2025.01.04
24 네이버 부스트캠프 지원 회고  (2) 2024.07.10

이전 포스트에서 모바일에서의 클릭 이벤트로 인해 6가지 이벤트가 연쇄적으로 발생하는 것을 발견했습니다.

touchstart → touchend → mousemove → mousedown → mouseup → click

 

이번에는 다양한 상황에서의 클릭 이벤트의 시퀸스를 테스트해보고 정리해보겠습니다.

또한, 여러가지 상황에 따른 이벤트 시퀸스가 다른 이유에 대해서도 탐구해보겠습니다.

 

먼저, 모바일과 데스크톱 상황에서의 클릭 이벤트는 서로 다르게 적용됩니다.

데스크톱은 마우스를 움직이는 도중에 특정 시점에서의 클릭으로 이벤트가 발생하지만, 모바일에서는 마우스는 존재하지 않고 특정 시점의 유저 손가락 터치로 이벤트가 발생하기 때문입니다.

 

따라서, 먼저 데스크톱 상황에서 발생하는 이벤트에 대해 살펴보겠습니다.

 

 

실험 방법

  1. 크롬 브라우저를 토대로 실험하였습니다.
  2. 클릭 이벤트와 관련된 이벤트를 body 에 eventListener 를 등록한 이후, 이벤트 발생시 로그를 찍도록 만들었습니다.
  useEffect(() => {
    ;['touchstart', 'touchmove', 'touchend', 'mousedown', 'mousemove', 'mouseup', 'click', 'contextmenu'].forEach(eventType => {
      document.addEventListener(eventType, event => {
        console.log(`Event detected: ${eventType}`)
        console.log(`Event target:`, event.target)
      })
    })
  }, [])

 

 

데스크톱 클릭 이벤트

데스크톱에서의 클릭 이벤트는 간단합니다.

마우스 클릭시 mousedown → mouseup → click 순으로 이벤트가 발생합니다.

데스크톱에서의 클릭 이벤트 시퀸스

 

모바일 클릭 이벤트

모바일에서의 클릭 시퀸스가 다양했습니다.

4가지 모바일 터치 이벤트 발생 상황에서의 이벤트 시퀸스는 다음과 같습니다.

모바일 환경 테스트를 위해서는 개발자 도구의 반응형 페이지를 관찰하는 “Toogle Device Toolbar” 기능으로 진행하였습니다.

 

 

1. 일반적인 터치

터치를 한 후에 마우스(손)을 이동하지 않고 빠른 시간 내에 원래 자리에서 떼는 경우입니다.

[1편] 에서 보았듯이, 6가지 이벤트 시퀸스가 연속적으로 발생하고 있습니다

  • touchstart > touchend > mousemove > mousedown > mouseup > click

일반적인 터치에서의 이벤트 시퀸스

 

2. 일반적인 터치 + 시간이 지난 후에 떼기

일반적인 터치 실험과 마찬가지로 제자리에서 누르고 떼는 동작을 실행하지만, 일정 시간이 지난 후에 떼는 경우입니다.

  • touchstart > mousemove > contextmenu

일반 터치 + 시간 지난 후 떼기 이벤트 시퀸스

 

특이하게도, 해당 경우 contextmenu 이벤트가 발생합니다.

이를 “long press” 라고 하며, 여러분은 이미 모바일에서 수 없이 많이 사용하고 있습니다.

 

예를 들어, 앱 아이콘을 꾹 눌러서 위치를 바꾼 경험이 있지 않으신가요?

바로 이러한 상황이 long press가 작동되고 있던 상황입니다.

 

Q. 일반 Touch와 Long Press를 구분하는 시간적 간격이 존재할 것 같은데요. 몇 초 인가요?

운영체제 마다 다르다고 합니다. 기본적으로 300 ~ 500ms 이지만, 변동의 여지가 있으며 핸드폰 사용자마다 이를 다르게 설정할 수 있는 기능이 있어, 확정할 수 없습니다.

ios에서 Long Press 인식 시간을 바꾸는 설정

3. 터치 후 이동

일반적인 터치 이후 옆으로 이동했을 때 발생하는 이벤트입니다.

중간의 touchmove 이벤트는 커서가 이동할 때 계속 발생합니다. (아래 예시는 한번만 나오도록 찍은 것)

  • touchstart > touchmove > touchend

터치 후 이동 이벤트 시퀸스

 

4. 두 손가락 터치

두 손가락 터치는 안드로이드의 유선 디버깅 환경에서 테스트하였습니다.

다음과 같은 시퀸스로 이벤트가 발생했습니다.

  • touchstart > touchstart > touchend > touchend

두 손가락 터치는 pinch-zoom(두손가락으로 확대,축소) 기능을 사용하기 위한 초기 단계로 이해하시면 되겠습니다.

 

💡 Pinch-Zoom에 대한 자세한 공부는 더 해봐야겠습니다.

 


 

이렇게 이벤트 시퀸스가 다른 이유는 무엇일까요?

앞서 살펴보았듯이, 모바일과 데스크톱 환경에서의 클릭(터치)는 매우 다른 성격을 지니고 있습니다.

 

모바일 환경에서의 클릭은 Context 메뉴를 여는 long-press, 확대/축소를 하는 Pinch-Zoom 등 다양한 상황으로의 전환 가능성이 있습니다.

 

따라서, 이벤트 중간에 시간적 간격을 두어 다양한 상황이 발생할 수 있는 시간적 임계치를 설정한 것입니다.

이러한 임계치는 브라우저마다 · 운영체제마다 다르다고 합니다.

 

현업에서 어떻게 모든 상황에 대처하는지 궁금해지네요. 아시는 분은 댓글로 남겨주시면 감사하겠습니다.

 

다음 편에는 학습한 지식을 토대로 제가 제작했던 외부 클릭 커스텀 훅을 제작했던 과정을 소개해드리겠습니다.

 

문제 상황

포토폴리오 프로젝트에서, 커스텀 훅 라이브러리인 `usehooks-ts`를 사용하여 헤더의 햄버거 버튼을 구현하고 있었습니다.

구현하고 싶었던 기능은 "햄버거 버튼을 클릭해서 헤더 메뉴바를 열고 난 뒤, 메뉴바 외부를 클릭 시 닫기"의 간단한 기능이었다.

문제는 아래 영상처럼 햄버거 버튼 클릭으로 모달이 열린 상태에서 외부 클릭을 했을 때 닫혔다가 다시 열리는 현상이 발생했다는 것이다.

(간단해 보이는 기능에서 오류가 발생하면 골때리는 거, 아시죠..?)

검은 화면은 죄송해요. 편집에는 서툴러서

 

🔖 해당 오류를 해결했던 트러블 슈팅 과정과 해결과정 (1편), 각종 클릭 이벤트의 시퀸스 (2편), 개인 NPM 커스텀 훅 개발 과정(3편)을 3편에 걸쳐 공유하고자 합니다.


기존 구현 방식

먼저, usehooks-ts 라이브러리는 리액트 ES6 기반의 커스텀 훅을 제공하는 유용한 라이브러리입니다.

 

저는 `usehookts-ts` 에서 햄버거 버튼을 눌러 메뉴바를 키고 닫기 위한 `useToggle` , 메뉴바의 외부를 클릭했을 때 지정한 DOM 요소를 닫히도록 설정하는 `useOnClickOutside` 2가지 Custom-Hook을 사용하였습니다.

  1. 헤더에 useToggle 훅을 사용하여, 메뉴바가 보이는 여부를 관리하였습니다. (코드가 직관적이라 이해하시기 편할겁니다)
// 상위 Header 컴포넌트
const Header = (): ReactNode => {
  const [value, toggle, setValue] = useToggle(false)
	  return (
    <header>
	  1️⃣ <LucideIcon name='Menu' size={35} className='lg:hidden' onClick={toggle} />
     <MobileNavBar
        isOpen={value}
        onClose={toggle}
        className='absolute right-1/2 top-full block translate-x-1/2 lg:hidden'
      />
    </header>
  )
}

 

2. 메뉴바에 (`MobileNavBar`) `useOnClickOutside` 훅을 설정하여, 메뉴바 외부를 클릭하면 메뉴바가 닫히도록 하였습니다.

[useOnClickOutside의 인자]

  • ref : 외부 클릭 감지를 적용할 대상
  • Function: 외부 클릭 감지하였을 경우 수행할 함수
const MobileNavBar = ({ isOpen, onClose, className }: MobileNavBarProps): ReactNode => {
	const ref = useRef<HTMLElement>(null)
2️⃣  useOnClickOutside(ref as RefObject<HTMLElement>, onClose)
  
  return (
      {isOpen && (
        <nav ref={ref}>
          <ul className='flex w-full flex-col items-center justify-start text-lg'>
            {LINKS.map((link, index) => (
              <li>{link}</li>
            ))}
          </ul>
        </nav>
      )}
  )

 

이렇게 구현하였을 때, 처음 영상처럼  햄버거 버튼 위를 클릭하면 닫히고 바로 열리는 로직이 실행되며 메뉴바가 닫히지 않았습니다.

그러면 왜 발생했을까요?


트러블 슈팅 과정

몇 가지 정보를 토대로 가설과 검증을 통해 에러 원인을 찾아보았습니다.

  • 햄버거 버튼 위를 제외한 외부를 클릭하면 다시 열리지 않는다. (정상 작동한다)
  • (오류 발생 시) 외부 클릭 이벤트 → 햄버거 버튼 클릭 이벤트 순으로 이벤트가 실행된다. (콘솔 확인)

이를 토대로, 내린 가설은 “외부 클릭 이벤트로 메뉴바가 닫히고, 바로 햄버거 버튼 이벤트가 실행되나?” 였습니다.

 

(엥? 나는 클릭을 분명히 한번만 했는데?)

 

 

해당 가설을 확인해보기 위해, 이벤트의 종류를 담는 `event.type`을 각각 이벤트 실행함수에 담아 같이 실행해보았더니, 각각 아래와 같은 이벤트가 실행되었습니다.

  • 햄버거 버튼 이벤트: Click Event
  • 외부 클릭 이벤트: mousedown Event
mousedown Event가 등장하는 이유는 해당 문제가 모바일 환경에서의 트러블 슈팅을 담았기 때문입니다. 😁

또한, `useOnClickOutside` 가 mousedown Event를 발생시키는 이유는 라이브러리 코드 자체가, mousedown 이벤트를 세 번쨰 인자가 없을 시, 디폴트 이벤트로 선택하고 있었습니다.
// src/useOnClickOutside/useOnClickOutside.ts
function useOnClickOutside(ref, handler, ⭐ eventType = "mousedown" ⭐, eventListenerOptions = {}) {
  // 라이브러리 내부 코드입니다. 
  useEventListener(
    eventType,
    (event) => {
      const target = event.target;
      if (!target || !target.isConnected) {
        return;
      }
      const isOutside = Array.isArray(ref) ? ref.filter((r) => Boolean(r.current)).every((r) => r.current && !r.current.contains(target)) : ref.current && !ref.current.contains(target);
      if (isOutside) {
        handler(event);
      }
    },
    void 0,
    eventListenerOptions
  );
}​

 

문제는, 분명 한번을 클릭 했는데 mousedown AND click 총 2번의 이벤트가 발생하는 것이 이해가 되지 않아, 모든 클릭 이벤트에 대한 로그를 아래 코드로 찍어보았습니다.

// Header 컴포넌트
  useEffect(() => {
    ;['touchstart', 'touchmove', 'touchend', 'mousedown', 'mousemove', 'mouseup', 'click'].forEach(eventType => {
      document.addEventListener(eventType, event => {
        console.log(`Event detected: ${eventType}`)
        console.log(`Event target:`, event.target)
      })
    })
  }, [])

 

이후, 에러가 발생했던 상황을 재현해보았습니다.

그 결과, 메뉴창을 닫기 위한 클릭 한 번에 총 6번의 이벤트가 발생했습니다.

 

mousemove 이벤트에서 외부 클릭 이벤트가 실행되며, click 이벤트에서 햄버거 버튼 이벤트가 실행되는 것을 관찰할 수 있습니다.

touchstart → touchend → mousemove (외부 클릭 이벤트) → mousedown → mouseup → click (햄버거 버튼)

 

콘솔로 찍어본 이벤트 실행 순서

 

그렇다면, “외부 클릭 이벤트가 발생하는 mousemove 이벤트에 event.preventDefault()로 하위 이벤트를 막자” 고 생각하였고 이를 아래와 같이 시도했지만, 여전히 마지막 클릭 이벤트가 실행되었습니다.

 

// 상위 Header 컴포넌트
const Header = (): ReactNode => {
  const [value, toggle, setValue] = useToggle(false)
  
  🔖
  const NavbarOutsideEvent = (e) => {
	  e.preventDefault()
	  toggle()
  }
  
	  return (
    <header>
	  1️⃣ <LucideIcon name='Menu' size={35} className='lg:hidden' onClick={toggle} />
     <MobileNavBar
        isOpen={value}
        onClose={NavbarOutsideEvent}
        className='absolute right-1/2 top-full block translate-x-1/2 lg:hidden'
      />
    </header>
  )
}

 

많은 질문들, 블로그을 서핑하다가 결국 W3C 공식문서에서 이 해답을 찾을 수 있었습니다.

 

공식문서에서는 이렇게 언급합니다.

 

If the user agent intreprets a sequence of touch events as a click, then it should dispatch mousemove, mousedown, mouseup, and click events (in that order) at the location of the touchend event for the corresponding touch input. 

If the preventDefault method of touchstart or touchmove is called, the user agent should not dispatch any mouse event that would be a consequential result of the the prevented touch event.

 

"touch 이벤트 다음에, mouse이벤트, click이벤트가 실행되며, touch 이벤트에 preventDefault를 설정하면, 이후에 발생할 이벤트를 막을 수 있다"

 

 

GPT에게도 질문을 통해 확인해본 결과

터치 이벤트의 경우, 기본 동작에는 다음이 포함됩니다. 이때, 마우스 이벤트를 생성하는 것은 터치 이벤트 이므로, 이에 해당하는 `preventDefault`를 해주어야 마우스 이벤트 자체를 방지할 수 있습니다.
- 화면 스크롤
- 확대 / 축소
- 마우스 이벤트 생성

 

해결방법

이를 토대로, 기존 코드에서 `useOnClickOutside` 의 세 번째 인자로 이벤트를 `touchstart`로 변경하였더니 다음과 같은 오류가 발생했습니다.

useOnClickOutside(ref as RefObject<HTMLElement>, onClose, 'touchstart')

 

passive 설정으로 인한 오류

addEventListener의 세 번째 인자의 옵션 중 하나인 `passive` 옵션은 false를 기본값으로 가지지만 일부 브라우저(특히 크롬과 파이어폭스)에서는 스크롤 성능 향상을 위해서 touchstart touchmove 이벤트에 대해 기본값이 true로 설정합니다.

 

만약 `touchstart` 이벤트를 사용한다고 했을 때, passive 옵션이 true로 설정되어 있으면 콜백 함수 내부에서 `preventDefault()`를 호출해도 콘솔 경고만 출력할 뿐 preventDefault()가 제대로 동작하지 않는다고 합니다.

 

따라서, `touchstart` 이벤트 대신, `touchend` 이벤트로 바꾸어 실행하였더니 목표했던 기능에 성공하였습니다.

기능구현에 성공한 모습 야호

 

마무리

이렇게, 클릭과 관련된 이벤트로 인해 발생했던 문제점과 해결방안을 알아보았습니다.

하지만, 에러를 해결했어도 에러가 발생했던 자세한 이유를 살펴보아야 제대로 해결했다고 할 수 있습니다.

 

2편에는 모바일 환경에서 단순 클릭에 이벤트가 왜 6번이나 발생했는지에 대해 살펴보고, 다양한 클릭 상황의 이벤트 발생 시퀸스를 알아보겠습니다.

(3편은 해당 내용을 반영하여 개선된 커스텀 훅 NPM 패키지 제작 과정입니다)

+ Recent posts