네이티브 위챗 미니프로그램에서 터치 이벤트를 활용한 스와이프 삭제 기능 구현

스와이프 삭제 기능의 핵심 원리

리스트 항목을 옆으로 밀어 삭제 버튼을 노출하는 UI는 모바일 환경에서 매우 일반적인 상호작용입니다. 위챗 미니프로그램(WeChat Mini Program)에서는 movable-view 컴포넌트를 사용할 수도 있지만, 터치 이벤트(touchstart, touchmove, touchend)와 CSS transform 속성을 결합하면 더 세밀한 제어와 부드러운 애니메이션을 구현할 수 있습니다.

WXML 구조 설계

각 리스트 아이템은 콘텐츠 영역과 삭제 버튼 영역을 가로로 나란히 배치합니다. 전체 래퍼의 overflow: hidden 속성을 이용해 초기에는 삭제 버튼이 화면 밖으로 숨겨지도록 하고, 터치 이동 거리에 따라 콘텐츠 영역을 왼쪽으로 이동(Translate)시킵니다.

<view class="swipe-list">
  <view 
    class="swipe-item" 
    wx:for="{{records}}" 
    wx:key="id"
    bindtouchstart="handleTouchStart"
    bindtouchmove="handleTouchMove"
    bindtouchend="handleTouchEnd"
    data-index="{{index}}"
  >
    <view class="swipe-content" style="transform: translateX({{item.translateX}}px); transition: {{item.transition ? 'transform 0.2s ease-out' : 'none'}}">
      <view class="main-text">{{item.name}}</view>
      <view class="action-delete" catchtap="handleDelete" data-index="{{index}}">삭제</view>
    </view>
  </view>
</view>

JavaScript 로직 구현

터치 시작 지점을 기록하고, 손가락을 움직이는 동안 실시간으로 X축 이동 거리를 계산하여 translateX 값을 업데이트합니다. 터치가 끝났을 때는 이동 거리가 임계값(threshold)을 넘었는지에 따라 완전히 열리거나 원래 위치로 복귀하도록 트랜지션을 적용합니다.

Page({
  data: {
    records: [
      { id: 101, name: '첫 번째 항목', translateX: 0, transition: false },
      { id: 102, name: '두 번째 항목', translateX: 0, transition: false },
      { id: 103, name: '세 번째 항목', translateX: 0, transition: false }
    ],
    touchStartX: 0,
    touchStartY: 0,
    deleteBtnWidth: 80 // 삭제 버튼의 너비 (px)
  },

  handleTouchStart(e) {
    const touch = e.touches[0];
    this.setData({
      touchStartX: touch.clientX,
      touchStartY: touch.clientY
    });
  },

  handleTouchMove(e) {
    const touch = e.touches[0];
    const deltaX = touch.clientX - this.data.touchStartX;
    const deltaY = touch.clientY - this.data.touchStartY;
    
    // 수직 스크롤과 충돌 방지
    if (Math.abs(deltaY) > Math.abs(deltaX)) return;

    const index = e.currentTarget.dataset.index;
    let newTranslateX = deltaX;
    
    // 왼쪽으로 밀 때 최대 이동 거리 제한
    if (newTranslateX < -this.data.deleteBtnWidth) {
      newTranslateX = -this.data.deleteBtnWidth;
    }
    // 오른쪽으로 밀 때 원래 위치를 벗어나지 않도록 제한
    if (newTranslateX > 0) {
      newTranslateX = 0;
    }

    this.setData({
      [`records[${index}].translateX`]: newTranslateX,
      [`records[${index}].transition`]: false
    });
  },

  handleTouchEnd(e) {
    const index = e.currentTarget.dataset.index;
    const currentX = this.data.records[index].translateX;
    const threshold = -this.data.deleteBtnWidth / 2;
    
    // 임계값을 넘으면 완전히 열림, 아니면 닫힘
    let finalX = (currentX < threshold) ? -this.data.deleteBtnWidth : 0;

    this.setData({
      [`records[${index}].translateX`]: finalX,
      [`records[${index}].transition`]: true
    });
  },

  handleDelete(e) {
    const index = e.currentTarget.dataset.index;
    const updatedRecords = this.data.records.filter((_, i) => i !== index);
    this.setData({
      records: updatedRecords
    });
  }
});

WXSS 스타일링

삭제 버튼이 초기에 보이지 않도록 아이템 컨테이너에 overflow: hidden을 적용합니다. 콘텐츠 영역의 너비는 화면 너비에 삭제 버튼의 너비를 더한 값으로 설정하여 가로 스크롤이 발생하지 않도록 합니다.

.swipe-list {
  width: 100%;
  padding: 20rpx;
  box-sizing: border-box;
  background-color: #f5f5f5;
}

.swipe-item {
  width: 100%;
  margin-bottom: 20rpx;
  overflow: hidden;
  border-radius: 16rpx;
  background-color: #fff;
}

.swipe-content {
  display: flex;
  width: calc(100% + 80px); /* 삭제 버튼 너비만큼 추가 */
}

.main-text {
  flex: 1;
  padding: 30rpx;
  font-size: 32rpx;
  color: #333;
  display: flex;
  align-items: center;
  background-color: #fff;
}

.action-delete {
  width: 80px;
  display: flex;
  justify-content: center;
  align-items: center;
  background-color: #ff4d4f;
  color: #fff;
  font-size: 28rpx;
  font-weight: bold;
}

태그: WeChat Mini Program WXML WXSS Touch Events Swipe to Delete

7월 18일 23:50에 게시됨