task flow
- image-crop-picker로 얻은 이미지 객체를 FormData에 담기
(사진을 찍거나 갤러리에서 사진을 선택하면 프로미스 콜백의 인자로 image라는 객체를 던저줌)- formData 객체를 서버로 전송
- form에 있는 데이터들과 images에 담겨있는 이미지들을 서버에 전송해준다.
Ask.tsx
const [images, setImages] = useState<Array<ImageModel.IImages>>([]);
<ImageForm
images={images}
onUploaded={(image) => {
setImages([...images, image])
}}
onDeleted={(image) => {
setImages(produce(images, draft => {
remove(draft, (i) => image.id === i.id);
}))
}}
/>
interface IProps {
images: Array<ImageModel.IImages>;
onUploaded: (image: ImageModel.IImages) => void;
onDeleted: (image: ImageModel.IImages) => void;
style?: StyleProp<ViewStyle>;
}
<View style={style}>
<FlatList
data={images.concat(getDummyImages())}
renderItem={renderItem}
keyExtractor={(item) => String(item.id)}
horizontal={true}
scrollEnabled={false}
/>
{isLoading &&
<Container>
<FullScreenLoading />
</Container>}
</View >
현재 진행하는 프로젝트에서는 최대 3개까지만 이미지 첨부가 가능하지만, 나중에는 여러개의 이미지를 첨부할 수도 있어 FlatList를 사용하였다.
const getDummyImages = (): Array<ImageModel.IImages> => {
if (images.length < 3) {
return map(range(0, 1), () => {
return {
'key': '',
'id': uniqueId(),
'uuid': '',
'createdAt': '',
'updatedAt': '',
'title': '',
'description': '',
'fileName': '',
'fileType': '',
'serviceUrl': '',
}
})
} else if (images.length === 3) {
return map(range(0), () => {
return {
'key': '',
'id': uniqueId(),
'uuid': '',
'createdAt': '',
'updatedAt': '',
'title': '',
'description': '',
'fileName': '',
'fileType': '',
'serviceUrl': '',
}
})
}
return images;
}
props로 받은 빈 배열의 images에 getDummyImages라는 문자열을 합쳐 하나의 배열로 만들어 주었다. (선택한 이미지들이 images라는 빈 객체에 하나씩 추가되기 위해서)
만약, images.length가 3보다 작다면 이미지 첨부할때마다 하나씩 빈 이미지가 뜨고

images.length가 3이라면 더이상 빈 이미지가 뜨지 않도록 하였다.

lodash는 Javascript의 인기있는 라이브러리중 하나인데
arrray, data 등 데이터의 필수적인 구조를 쉽게 다를 수 있게끔 하는데 사용된다. JavaScript의 코드를 줄여주고 빠른 작업에 도움이 된다고 한다.
_.range(start, end, step);
인자를 주면 그 범위만큼 순차 배열을 생성해주며
오름차순이나 내림차순 숫자로 이뤄진 배열을 만들 때 쓴다.
_.map(Collection, fieldName)
순회하여 얻은 값들을 모아서 배열로 반환시켜준다.
객체의 특정 key의 value들을 모아 배열로 만들 때 유용하다.
const renderItem = ({ item, index }: { item: ImageModel.IImages, index: number }) => {
//이미지 첨부 했을 때
const ActiveImage = (
<View key={index} style={{ marginRight: 10 }}>
<Image
source={{ uri: item.serviceUrl }}
style={{ width: 74, height: 74, borderRadius: 5.7 }}
/>
{selectedImage &&
<GrayFullfilClose
style={{ position: 'absolute', top: 6, right: 6 }}
width={14}
height={14}
onPress={() => {
Alert.alert(
'해당 사진을 삭제하시겠습니까?',
'',
[
{
text: '취소',
style: 'cancel',
},
{
text: '삭제',
onPress: async () => {
onDeleted(item)
}
}
]
);
}}
/>
}
</View>
)
//이미지 첨부 안했을 떄
const EmptyImage = (
<ImageContainer
onPress={() => {
if (Platform.OS === 'ios') {
request(PERMISSIONS.IOS.PHOTO_LIBRARY).then((result) => {
switch (result) {
case RESULTS.GRANTED: //권한부여
ImageCropPicker.openPicker({
mediaType: 'photo',
}).then(image => {
onUpload(image)
}).catch((e) => { console.log(e) });
break;
default:
Alert.alert(
'사진첨부를 원하시면 ‘설정’을 눌러 ‘사진’ 접근을 허용해주세요.',
'',
[
{
text: '취소',
style: 'cancel',
},
{
text: '설정',
onPress: () => {
openSettings();
}
}
]
)
}
})
} else if (Platform.OS === 'android') {
request(PERMISSIONS.ANDROID.CAMERA).then((result) => {
switch (result) {
case RESULTS.GRANTED:
ImageCropPicker.openPicker({
mediaType: 'photo',
cropping: true
}).then(image => {
onUpload(image)
}).catch((e) => { console.log("e", e) });
break;
default:
Alert.alert(
'사진첨부를 원하시면 ‘설정’을 눌러 ‘사진’ 접근을 허용해주세요.',
'',
[
{
text: '취소',
style: 'cancel',
},
{
text: '설정',
onPress: () => {
openSettings();
}
}
]
);
}
});
}
}}
>
<ImageContainerDetailStyle />
<ImageContainerDetailStyle style={{ transform: [{ rotate: '90deg' }], bottom: 1 }} />
</ImageContainer>
)
return item.serviceUrl ? ActiveImage : EmptyImage
}

다음으로는 서버에 이미지 파일을 보내는 작업이 필요하다.
FormData 객체는 폼의 각 필드와 값을 나타내는 키/값 쌍들의 집합을 쉽게 구성할 수 있는 방법을 제공한다.
이를 이용하면 데이터를 multipart/form-data 형식으로 전송할 수 있다.
반드시 uri, name, type을 같이 전송해줘야하는데
type은 실험해보니 'multipart/form-data'도 가능하고 'image/jpeg'도 가능하다.
세가지 속성 중 하나라도 없으면 네트워크 에러가 난다.
ImageForm.tsx
//사진 업로드
const onUpload = async (image) => {
setLoading(true);
const formData = new FormData()
formData.append('file', {
uri: image.path,
name: `${uniqueId('image')}.jpg`,
type: image.mime
})
const { status, data } = await UploadFileService.File.upload(formData);
if (status === 201) {
onUploaded(data)
} else {
Toast.show({
text1: data?.message,
type: ConfigType.Toast.ErrorToast,
});
}
setLoading(false)
}
UploadFileService.ts
export namespace UploadFileService {
export const File = {
upload: async (files: FormData) => {
try {
const config: AxiosRequestConfig = {
headers: { "Content-Type": "multipart/form-data" },
};
const { data, status } = await AxiosContext.post(
"/api/upload/inquiry",
files,
config,
);
return { data: data, status: status };
} catch (error) {
return { error: error };
}
},
};
}


마지막으로 form에 있는 데이터들과 images에 담겨있는 이미지들을 서버에 전송해주면 된다.
Ask.tsx
<Container>
<HeaderNavigation
hasBackButton={true}
title="문의하기"
onPressBackButton={() => { navigation.goBack() }} />
<LineBorderBottom />
<View style={{ marginHorizontal: 16, flex: 1, justifyContent: 'space-between' }}>
<FormProvider {...form}>
<View>
<View style={{ flexDirection: 'row', }}>
<Input
name="contact"
rules={{ required: true }}
keyboardTypeOption={'default'}
placeholder={'답변을 받으실 메일 주소나 전화번호를 입력해 주세요'}
onChangeText={(e) => { setContact(e) }}
style={{ marginTop: 20, width: '100%' }} />
<TouchableOpacity
onPress={() => { setValue('contact', '') }}
style={{ justifyContent: 'center', paddingRight: 16 }}>
{value &&
<Title
font={FontStyle.regular.font12}
text="수정하기"
color={Color.Gray80}
style={{ position: "absolute", top: 32, right: 26 }} />
}
</TouchableOpacity>
</View>
<Title
font={FontStyle.bold.font14}
color={Color.Gray80}
text={"문의 내용"}
style={{ marginBottom: 20, marginTop: 35 }}
/>
<Input
name="title"
keyboardTypeOption={'default'}
placeholder={'제목을 입력해 주세요 (최대 80자)'} />
<Textarea
name="content"
rules={{ required: true }}
placeholder={'문의 내용을 입력해 주세요 (최대 300자)'}
height={190}
/>
<Title font={FontStyle.bold.font14} text="이미지 첨부" color={Color.Gray80} style={{ marginTop: 35, marginBottom: 20 }} />
<ImageForm
images={images}
onUploaded={(image) => {
setImages([...images, image])
}}
onDeleted={(image) => {
setImages(produce(images, draft => {
remove(draft, (i) => image.id === i.id);
}))
}}
/>
<Title font={FontStyle.regular.font12} color={Color.Gray60}
text={"이미지는 JPEG, JPG, PNG만 가능하며\n최대 3개까지 첨부 가능합니다."} style={{ marginTop: 10 }} />
</View>
<DefaultButton
text="문의하기"
onPress={onSubmit}
disabled={!isValid}
/>
</FormProvider>
</View>
</Container>
const onSubmit = async () => {
setLoading(true);
const model = {
...getValues(),
fileUuids: map(images, p => p.uuid)
} as UserModel.IInquiryModel;
const { status } = await AccountService.Inquiry.create(model);
if (status === 201) {
navigation.goBack();
} else {
Alert.alert('입력된 정보를 다시한번 확인해주세요');
}
setLoading(false)
}
AccountService.ts
export const Inquiry = {
create: async (model: UserModel.IInquiryModel) => {
try {
const { data, status } = await AxiosContext.post(`/api/user/inquiry`, model)
return { data: data, status: status }
} catch (error) {
return { data: null, error: error }
}
},
}
저도 다중 이미지 업로드 기능을 구현하고 있어서 전체 코드를 확인하고 싶은데, repo 공유 가능하신가요??