React Naitve에서 SwipeableListView을 이용한 샘플 앱 구현

제가 개발하고 있는 앱에서 필요한 기능 중에 하나는 현재 재생 목록을 편집하는 것이었습니다. 그 중에서도 중요한 것이 재생목록에서 불필요한 음악을 제거하는 것인데요. 현재 서비스 중인 다른 앱들에서 이 기능에 대한 동작은 어떠한지 알아보내 대략 아래 세가지 인 것 같았습니다.

  1. 해당 곡이 있는 부분을 좌측으로 밀어서 숨겨진 삭제 버튼 선택
  2. 곡을 선택하면 서브메뉴가 나오고 삭제 버튼을 선택
  3. 편집 모드로 전환 후에 삭제할 곡을 선택하고 삭제 버튼 선택

2, 3 번의 경우 오래된 스타일이거나 최종 동작으로 가는 동안 시선 분산이 일어나서 사용자에게 별로라는 생각이 들어서 1번으로 결정하고 이미 만들어진 라이브러리가 있는지 찾아봤을 때 두 가지 선택지가 있었습니다.

어떤 걸 써야하나 고민하던 중에 v0.27.0에서 관련 컴포넌트가 추가되었다는 답변을 찾았습니다. 👍

stackoverflow

해당 컴포넌트는 아직(0.43)까지도 공식적으로 배포되고있지 않고 Experimental 형태로 존재하고 있습니다.

https://github.com/facebook/react-native

SwipeableListView는 ListView를 확장한 형태이기 때문에 swipe 관련한 동작을 위한 구현외에는 고기본적인 사용법은 유사합니다.

DataSource 구성

가장 먼저 해야 할 것이 DataSource를 만드는 것인데 만드는 방법이 기존의 ListView와는조금 다릅니다.

아래는 ListView 의 일반적인 초기 DataSource 생성 방법입니다.

const ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 });
this.state = {
dataSource: ds.cloneWithRows(tracks),
};

아래는 SwipeableListView 의 DataSource 생성 방법입니다.

const ds = SwipeableListView.getNewDataSource();
this.state = {
dataSource:
ds.cloneWithRowsAndSections(...this.genDataSource(tracks)),
};

여기서 두 가지 다른 점을 발견할 수 있는데 하나는 DataSource 객체를 만드는 방법이고 다른 하나는 SwipeableListView 에서는 cloneWithRows 를 지원하지 않는다는 것입니다. 그래서 dataBlob, sectionIdentities, rowIdentities를 모두 넘겨줘야 하고 이를 genDataSource 라는 함수에서 만들어줍니다.

genDataSource(rowData: Array<any>) {
const dataBlob = {};
const sectionIDs = ['Section 0'];
const rowIDs = [[]];

/**
* dataBlob example below:
{
'Section 0': {
'Row 0': {
id: '0',
text: 'row 0 text'
},
'Row 1': {
id: '1',
text: 'row 1 text'
}
}
}
*/
dataBlob['Section 0'] = {};
rowData.forEach((el, index) => {
const rowName = `${index}`;
dataBlob[sectionIDs[0]][rowName] = {
id: rowName,
...el,
};
rowIDs[0].push(rowName);
});
return [dataBlob, sectionIDs, rowIDs];
}

중요한 것은 cloneWithRows 에서는 배열 자체를 dataBlob으로 넘겨주면 되는데 여기에서는 특정한 형태로 가공하는 과정을 거쳐야 한다는 것이고 이 형태는 위의 샘플 코드에서 확인하실 수 있습니다.

renderRow 및 renderQuickActions

renderRow는 사실 기존 ListView 와 동일하기 때문에 특별한 부분은 없습니다. 다만 만약 열려있는 상태에서 row를 클릭했을 때 row를 닫고 싶다면 아래와 같이 체크를 할 필요는 있습니다.

onSelectRow(rowID) {
const openID = this.state.dataSource.getOpenRowID();
if (openID && (openID === rowID)) {
this.setState({
dataSource: this.state.dataSource.setOpenRowID(null),
});
}
// Add more codes
}
renderRow(rowData, sectionID, rowID) {
const { title, artist, image_url } = rowData;
return (
<TouchableHighlight onPress={() => this.onSelectRow(rowID)}>
... 생략 ...
</TouchableHighlight>
);
}

getOpenRowID 는 현재 swipe동작으로 일해 열려있는 row에 대한 id를 반환해주는 함수입니다. 만약 열려있는 row가 없다면 null 를 반환하기 때문에 해당 값과 현재 선택된 rowID를 비교해서 setOpenRowID 의 값을 null로 설정하면 해당 row는 닫히게 됩니다.

swipe를 했을 때 보여지는 부분은 renderQuickActions 라는 prop에 함수를 넘겨주면 되는데 방법은 두 가지가 있습니다.

  1. 미리 정의된 컴포넌트 사용
  2. 사용자 정의 컴포넌트 사용

1번의 경우 SwipeableQuickActionsSwipeableQuickActionButton 을 조합해서 사용하면 되며 사용법은 아래와 같습니다.

renderStandardQuickActions(rowData: object, sectionID: string, rowID: string) {
return (
<SwipeableQuickActions>
<SwipeableQuickActionButton
imageSource={{ uri: 'https://unsplash.it/300/300?random' }}
imageStyle={styles.thumbnail}
onPress={() => { }}
text={'Action'}
textStyle={{ color: 'white' }}
/>
</SwipeableQuickActions>
);
}

그런데 위의 문제점은 보시는 것 처럼 SwipeableQuickActionButton 이미지와 텍스트를 포함하고 있고 이미지는 필수로 들어가야 하는 부분입니다. 때문에 무조건 이미지 소스를 설정해줘야합니다. (사실, 이런 방식으로 무엇을 만들려고 하는지 잘 모르겠네요.)

그래서 여기 예제에서는 임의로 액션 버튼을 만들어서 render함수를 통해 넘겨주도록 합니다.

onDeleteRow(index) {
tracks = [
...tracks.slice(0, index),
...tracks.slice(index + 1),
];
  const ds = SwipeableListView.getNewDataSource();
this.setState({
dataSource: ds.cloneWithRowsAndSections(...this.genDataSource(tracks)),
});
}
renderCustomQuickActions(rowData: object, sectionID: string, rowID: string) {
return (
<View style={styles.actionsContainer}>
<TouchableHighlight
style={[styles.actionButton, { backgroundColor: 'gold' }]}
underlayColor="transparent"
onPress={() => console.log(rowID)}>
<Icon name="pin" size={20} />
</TouchableHighlight>
<TouchableHighlight
style={[styles.actionButton, { backgroundColor: 'goldenrod' }]}
underlayColor="transparent"
onPress={() => this.onDeleteRow(parseInt(rowID))}>
<Icon name="trash" size={20} />
</TouchableHighlight>
</View>
);
}

QuickAction 중에 삭제버튼을 누르면 onDeleteRow 가 호출되는데 이 때 rowData를 조작해서 해당 데이터를 없앤 뒤에 dataSource를 다시 생성해서 setState 함수를 이용해서 다시 설정하면 됩니다.

Sample application

마치며…

오픈 소스 커뮤니티에 배포된 컴포넌트를 쓰면 애니메이션이나 기타 커스터마이즈 할 수 있는 부분이 있기는 하지만 유지가 제대로 안되거나 버그 수정이 원활하게 이루어지지 않는 경우가 있습니다. 그래서 만약 위에서 언급한 형태 정도의 요구사항이라면 React Naitve 내부에 있는 SwipeableListView를 쓰는 것도 나쁘지 않은 것 같습니다.

이 샘플 앱은 snack 을 통해서 확인해 볼 수 있습니다.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.