본문 바로가기

리액트 네이티브

[인스타그램 클론코딩] 사진 업로드 구현1 - SelectablePhoto

그냥 react-native-image-picker 사용하는게 편합니다.

 

메인 화면에서 + 버튼을 터치했을 때 나타나는 사진 선택기(SelectPhoto.js) 화면입니다.

사진 선택기는 2개의 컴포넌트로 구성되어 있습니다.

  • Image
    가장 최근에 터치한 이미지가 표시됩니다.
  • ScrollView
    기기의 모든 사진을 읽어들여서 각각의 사진을 SelectablePhoto에 표시합니다.

직접 만들어 볼 SelectablePhoto의 요건은 다음과 같습니다.

  • 선택(이미지), 미선택(회색 음영 + 체크 아이콘) 2가지 상태를 나타날 수 있어야 합니다.
  • 선택되었을 때 실행할 핸들러와 선택이 해제되었을 때 실행할 핸들러를 부모 컴포넌트로 부터 입력받아야 합니다.

지금부터 SelectablePhoto 컴포넌트를 만들어 보겠습니다.

함수형 컴포넌트로 만들었습니다.

우선 부모 컴포넌트(ScrollView)로 부터 크기(size), 이미지 주소(url), 터치했을 때 실행할 핸들러 2개를 입력받습니다.

  • onSelected는 해당 컴포넌트가 터치되었고, 현재 상태가 선택일 때 실행되는 핸들러입니다.
  • onDeselected는 해당 컴포넌트가 터치되었고, 현재 상태가 미선택일 때 실행되는 핸들러입니다.
export default ({onSelected, onDeselected, size, url}) => {
    ...
}

그리고 랜더링과 관련된 상태가 필요합니다. useState를 쓰기 딱 좋습니다.

const [isSelected, setIsSelected] = useState(false);

SelectablePhoto 컴포넌트가 터치됐을 때, 실행되는 핸들러입니다.

이 핸들러를 SelectablePhoto 컴포넌트를 구성하는 컴포넌트 중 최상위 컴포넌트(TouchableOpacity)의 onPress 속성에 입력해 줄 것입니다. 

const _onPress = () => {
    if(isSelected && onDeselected !== undefined) {
        onDeselected();
    }

    if(!isSelected && onSelected !== undefined) {
        onSelected();
    }

    setIsSelected(i => !i);
}

setIsSelected 함수로 상태를 변화시키전(랜더링 호출전), 변화될 상태에 대응하는 함수를 먼저 실행시키기 때문에 isSelected가 true일 때 onDeselected 함수를 실행시킵니다.

왜 이렇게 하나요?

setIsSelected 함수로 isSelected 값을 변경시키더라도 _onPress 안에서는 isSelected 값이 바뀐것을 인식하지 못합니다. 랜더러보다 다음 코드가 먼저 실행되기 때문이죠.

 

마지막으로 SelectablePhoto가 반환하는 컴포넌트입니다.

isSelected가 true일 때만 CheckCover 컴포넌트가 랜더링됩니다.

CheckCover는 이미지 위에서 회색 음영과 체크 아이콘을 표시하는 컴포넌트입니다.

 

 

 

Conatiner는 그냥 View 컴포넌트입니다.

CheckCover는 Container 컴포넌트 안에서 절대 경로(absolute)를 가지고, Container 컴포넌트 전체를 덮어 씌우는 컴포넌트입니다.

Ionicons 컴포넌트는 CheckCover 컴포넌트 안에서 가운데로 정렬됩니다.

const Container = styled.View`
`;

const CheckCover = styled.View`
    position: absolute;
    justify-content: center;
    align-items: center;
    top: 0;
    left: 0;
    opacity: 0.5;
    background-color: black;
    padding:5px;
`;

전체 코드는 제 github에서 확인할 수 있습니다.

그런데 SelectablePhoto 컴포넌트에는 사진에 대한 정보가 없는데, 어떻게 선택된 사진의 정보를 추출할 수 있을까요?

SelectablePhoto 외부에서 해결할 수 있습니다.

일단 장치에 있는 모든 사진을 가져옵니다. 저는 expo 환경에서 가져왔습니다. 만약 react-native-cli를 사용한다면 약간 다를 것입니다.

const [allPhotos, setAllPhotos] = useState();

const {assets} = await MediaLibrary.getAssetsAsync();
const [firstPhoto] = assets; // assets의 0번 인덱스를 firstPhoto 변수에 입력
setSelectedPhoto(firstPhoto);
setAllPhotos(assets);

MediaLibrary.getAssetsAsync 메소드는 다음과 같은 이미지 객체의 배열(assets)을 반환합니다.

여기서 필요한 것은 id uri 속성입니다.

Object {
  "albumId": "-1739773001",
  "creationTime": 1573187299375,
  "duration": 0,
  "filename": "20191108_132819.jpg",
  "height": 4656,
  "id": "11408", // !!
  "mediaType": "photo",
  "modificationTime": 1573187299000,
  "uri": "file:///storage/emulated/0/DCIM/Camera/20191108_132819.jpg", // !!
  "width": 2620,
}

SelectPhoto.js

맨 위에서 언급한 사진 선택기입니다.

이제 allPhotos 배열의 요소(이미지 객체) 단위로 SelectablePhoto 컴포넌트를 생성해 줍니다.

그리고 SelectablePhoto 컴포넌트의 onSelected와 onDeselected 속성에 addPhoto와 deletePhoto 함수를 그냥 넣어주는 것이 아니라, 매개값으로 아무것도 받지 않고 내부에서 addPhoto 또는 deletePhoto 함수를 실행하는 함수를 넣어주면 됩니다.

이때 addPhoto와 deletePhoto 함수에 컴포넌트를 생성할 때 사용했던 이미지 객체를 넣어주면, SelectablePhoto 외부에서 선택되거나 선택되지 않은 이미지를 수집할 수 있습니다.

 

 

SelectablePhoto를 외부와 최대한 독립시키기 위해서 위와 같이 구성했습니다.

참고로 setSelectedPhoto는 가장 최근에 선택된 이미지를 Image 컴포넌트에 출력하는 함수입니다.

전체 코드는 제 github에서 확인할 수 있습니다.

++ 보너스 ++

addPhoto 함수는 쉬우니까 deletePhoto 함수만 분석해 보겠습니다.

다음은 위의 deletePhoto 함수와 동일한 코드입니다.

findIndex 메소드를 사용해서 selectedPhotos 배열에서 함수의 매개값으로 입력된(사용자가 선택한) 이미지 객체의 id(photo.id)와 일치하는 요소(이미지 객체)의 인덱스(index)를 찾습니다. 

splice 메소드를 사용해서 위에서 구한 index부터 1개의 요소를 삭제합니다.

const deletePhoto = photo => {
    const index = selectedPhotos.findIndex(p => p.id === photo.id)
    selectedPhotos.splice(index, 1);
    setSelectedPhoto(photo);
}

++ 보너스2 ++

애초에 addPhoto 함수에서 photo를 selectedPhotos 배열에 그대로 저장하지 않고, { [photo.id]: photo } 형태로 변환해서 저장했으면, selectedPhotos[photo.id]를 사용해서 더 간단한 deletedPhoto 함수를 만들 수 있었을 것입니다.