사이드 프로젝트 개발 과정 - (이미지 선택 및 순서 변경 기능 구현)

knh6269·2024년 6월 13일
1

ootd.zip

목록 보기
10/16
post-thumbnail

도입

인스타그램, 페이스북등을 보면 유저가 선택한 사진의 순서를 바꿀 수 있다.
ootdzip에도 해당 기능을 통해 유저가 사진 순서를 바꿀 수 있게 해주기로 했다.


요구사항

디자인

기능

  • 유저는 갤러리에서 사진을 선택한다.
  • 유저가 갤러리에서 선택한 순서대로 기본 순서를 지정해준다.
  • 큰 화면에는 현재 선택한 이미지가 확대된다.
  • 이미지 선택 시 현재 선택한 사진이 아니라면 확대만 시켜준다.
  • 이미지 선택 시 현재 선택한 사진이라면 선택 해제 한다.
  • 선택 해제시 해당 이미지 뒤의 순서들을 앞으로 한칸씩 당긴다.

갤러리 사진 선택

웹뷰에서 native로 message 전달

//갤러리 페이지 진입 시 'OOTD' 메시지 보냄
useEffect(() => {  
  	sendReactNativeMessage({ type: 'OOTD' }); 
  }, []);

expo-image-picker 실행

getphoto.tsx

import * as ImagePicker from "expo-image-picker";

const usegetPhotos = () => {
  const [status, requestPermission] = ImagePicker.useMediaLibraryPermissions();

  const getPhoto = async (target: string) => {
    try {
      //권한 설정
      if (!status?.granted) {
        const permission = await requestPermission();
        if (!permission.granted) {
          return null;
        }
      }

      // 사진 선택 실행
      const item = target !== "OOTD";
      let result = await ImagePicker.launchImageLibraryAsync({
        mediaTypes: ImagePicker.MediaTypeOptions.Images,
        aspect: [1, 1],
        quality: 0.1,
        allowsMultipleSelection: !item, //OOTD는 여러장 선택 가능, 옷은 한장만 선택 가능
      });
    
      //사진 선택 취소
      if (result.canceled) {
        return null;
      } 
    
      return result;
    } catch (error) {
      console.error("에러 발생:121", error);
    }
  };

  return { getPhoto };
};

export default usegetPhotos;

Main.tsx

getSelectedPhoto(parseData.type).then(async (res) => { 
  const formData = new FormData();
  //이미지 개수만큼 진행
  res.assets.forEach((item) => {
    const type = `${item.type}/${item.uri.split(".").pop()}`;
    const localUri = item.uri;
    const name = localUri.split("/").pop();
    formData.append("images", {
      uri: localUri,
      type,
      name,
    } as unknown as Blob);
  });
 });

formData를 서버로 전송해 s3 주소 webview로 반환

fetch("https://ootdzip.com/api/v1/s3/image", {
    method: "POST",
    headers: {
      "Content-Type": "multipart/form-data",
      Authorization: `Bearer ${await SecureStore.getItemAsync(
        "accessToken"
      )}`,
    },
    body: formData,
  	})
    .then((response) => response.json())
    .then((response) => { 
    sendMessage({
      webViewRef,
      type: parseData.type,
      payload: response.result.imageUrls,
    });
  	})
    .catch((error) => {
    console.log("error", error);
  });
});

에러 발생1 - accessToken 만료 상황 발생

accessToken을 s3 업로드 api에 사용 해야하는데, 그 과정에서 액세스 토큰 만료가 되는 경우가 발생했다.
그래서 나는 갤러리 사진 선택 메세지를 보낼 때 토큰을 재발급 한 뒤 함께 보내 refresh 시키기로 했다.

const { getNewToken } = PublicApi();

const getToken = async () => {
  //토큰을 재발급 하는 함수
  await getNewToken();
  
  //액세스 토큰 보냄
  sendReactNativeMessage({
    type: 'accessToken',
    payload: localStorage.getItem('accessToken'),
  });
  
  //리프레시 토큰 보냄
  sendReactNativeMessage({
    type: 'refreshToken',
    payload: localStorage.getItem('refreshToken'),
  });
};

//갤러리 페이지 진입 시 토큰과 'OOTD' 메시지 보냄
useEffect(() => {
  	getToken(); 
  	sendReactNativeMessage({ type: 'OOTD' }); 
}, []);

에러발생2 - 이미지 크기 초과

이미지 크기가 너무 크다는 이유로 서버에서 api 요청이 실패했다는 메시지가 왔다. 최근 휴대폰 카메라들의 성능이 좋아지면서 사진의 크기가 많이 커진것이다. 이에 우리는 expo-image-pickeroption중 하나인 quality(0.1~1) 속성을 낮추기로 했다.
어디까지 낮춰야 화질의 변화가 많이 없을까싶어 테스트를 진행 해 보았는데 0.1까지 낮춰도 충분히 잘 보이는걸로 확인되었다.
실제로 파일의 크기도 많이 작아졌고 성공 응답을 받을 수 있었다. 하지만 며칠뒤 스크린샷 이미지들에서 에러가 발생한다는 소식을 들었고 확인해보았는데, error [SyntaxError: JSON Parse error: Unexpected character: <] 에러 메세지가 발생했다.
이미지 업로드 api 후 서버의 요청을 json으로 변환하는 과정을 거치는데 해당 과정에서 에러가 난 것 같았다.
그래서 해당 코드에서 console.log(response)를 해보니 아래와 같은 로그가 나왔다.
status:413에 대한 자료를 찾아보니 nginx 데이터 용량 초과였고, nginx 데이터 용량 설정을 따로 하지 않으면 1mb로 제한되어있다는 사실을 알게 되었다. 제한 용량을 늘려주었고, 잘 작동하게 되었다..!


에러발생3 - 비공개 테스트 안드로이드 사진 업로드 에러

Expo go를 활용해 테스트를 진행할 때는 모든것이 순조로웠다. 그리고 비공개 테스트.. 안드로이드 기기를 가진 친구에게 연락이 왔다. "사진 선택이 안돼" 그래서 급히 내 안드로이드 기기로 비공개 테스트 진행해보았는데, 안된다. 바로 Expo go로 실행해 보았는데 잘된다. ???? 물음 표에 빠진 나는 백엔드 강민이와 한시간 가량 모든 경우의 수를 찾았다. 그런데 아무리 찾아도 오류가 보이지 않았고, 내일 다시 하자 라는 말을 하기 직전 눈에 보이는 http.. 도메인을 연결하기 전 http로 api를 보내고 있었다. 그래서 이거 때문이구나 하는 생각으로 https로 바꾸자 정상 작동하였다. 아무래도 플레이 스토어에서 http로의 호출을 막고 있는게 아닌가 생각이 된다.


선택한 사진 편집

필요한 상태

  const [selectedImage, setSelectedImage] = useState<ImageWithTag>([]); //선택한 이미지 리스트
  const [realTouch, setRealTouch] = useState<number>(100); // 현재 선택한 인덱스

큰 이미지 구현

선택된 사진이 없을때 default 이미지 렌더링

선택된 이미지가 없을때 회색 이미지를 기본으로 렌더링해준다.

 {selectedImage.length === 0 && (
   <S.BigImage>
     <NextImage
       className="bigImage"
       src={defaultImage}
       alt="basic"
       fill={true}
     />
   </S.BigImage>
 )}

현재 선택한 인덱스의 이미지 렌더링

인덱스를 비교해 현재 선택된 인덱스의 사진을 렌더링 해준다.

{selectedImage &&
  selectedImage.map((item, index) => {
  
  if (item.ootdId === realTouch)
    return (
      <S.BigImage key={index}>
        <NextImage
          className="bigImage"
          src={item.ootdImage}
          alt="bigImage"
          fill={true}
          />
      </S.BigImage>
    );
})}

작은 이미지 리스트 구현

이미지 리스트

{imageAndTag &&
 imageAndTag.map((item, index) => {
    return (
      <S.SmallImage key={index} state={item.ootdId === realTouch}>
        <NextImage
          className="smallImage"
          onClick={() => onClickImage(item.ootdId, item.ootdImage)}
          src={item.ootdImage}
          alt="smallImage"
          fill={false}
          width={106}
          height={106}
          />
      </S.SmallImage>
    );
})}

이미지 리스트 클릭 이벤트

realTouch와 현재 클릭한 이미지가 같은 경우 ⭕

선택한 이미지 리스트에서 해당 ootdId를 가지고 있는 이미지를 제외한다.
만약 선택한 이미지 리스트의 개수가 한개뿐이라면 realTouch 상태를 100으로 업데이트 한다.

const onClickImage = (ootdId: number, ootdImage: string) => {
    const newSelectedImage = selectedImage.filter((item) => {
      if (item.ootdId !== ootdId) {
        return item;
      }
    });

    if (selectedImage.length === 1) {
      setRealTouch(100);
    } else {
      setRealTouch(newSelectedImage[newSelectedImage.length - 1].ootdId);
    }

    setSelectedImage(newSelectedImage);
  };

realTouch와 현재 클릭한 이미지가 다른 경우 ❌

선택한 이미지를 이미지 선택 리스트에 마지막 인덱스로 업데이트

if (ootdId !== realTouch) {
      setRealTouch(ootdId);

      const alive = selectedImage.filter(
        (item) => item.ootdId === ootdId
      ).length;

      if (!alive) {
        setRealTouch(ootdId);
        setSelectedImage([
          ...selectedImage,
          { ootdImage: ootdImage, ootdId: ootdId },
        ]);
      }
      return;
    }

이미지 순서 표기

만약 해당 이미지의 id가 선택된 이미지에 포함 되어있다면 인덱스 표시, 그렇지 않다면 빈 숫자 표시

{imageAndTag &&
 imageAndTag.map((item, index) => {
    const matchingIndex = selectedImage.findIndex(
      (items) => item.ootdId === items.ootdId
    );
  
    const isMatched = matchingIndex !== -1;

    return (
        <S.ImageNumber
          onClick={() => onClickImage(item.ootdId, item.ootdImage)}
          state={isMatched}>
          {isMatched ? (
            <Caption1>{matchingIndex + 1}</Caption1>
          ) : ('')}
        </S.ImageNumber>
    );
})}

완성 화면


정리

webview에서 native의 갤러리의 사진을 선택하고 순서를 변경하는 기능을 구현했다. 역시 native가 섞이니 더 어려운 것 같다. 그래도 완성하고 나니 뿌듯하다!

0개의 댓글