강의

멘토링

로드맵

인프런 커뮤니티 질문&답변

Sckroll님의 프로필 이미지
Sckroll

작성한 질문수

React Native with Expo: 제로초에게 제대로 배우기

이미지 선택하기, 촬영하기, 저장하기

사진 촬영 후 갤러리 저장 시 권한 문제

작성

·

20

0

안녕하세요, 제로초님! 강의 잘 듣고 있습니다.

다름이 아니라 ImagePicker로 카메라 사진 촬영 후 MediaLibrary를 사용해서 갤러리에 저장할 때, 다음과 같은 에러가 발생합니다.

expo-media-library-error.gifimage.png

확인해보니 AUDIO 권한이 선언되지 않았다고 하는데, 아래와 같이 app.jsonplugin에 권한을 추가해도 동일한 에러가 발생합니다.

{
    "expo": {
        // ...
        "plugins": [
            // ...
            [
                "expo-media-library",
                {
                    "photosPermission": "Allow $(PRODUCT_NAME) to access your photos.",
                    "savePhotosPermission": "Allow $(PRODUCT_NAME) to save photos.",
                    "isAccessMediaLocationEnabled": true,
                    "granularPermissions": ["audio", "photo"]
                }
            ]
        ],
        // ...
    }
}

제로초님이 올려주신 코드를 그대로 복사 & 붙여넣기 해봐도 같은 문제가 발생하는데, 혹시 제가 놓친 부분이 있을까요...? 강의에서 다룬 부분의 코드와 package.json 첨부하겠습니다.

import { FontAwesome, Ionicons } from '@expo/vector-icons';
import * as ImagePicker from 'expo-image-picker';
import * as Location from 'expo-location';
import * as MediaLibrary from 'expo-media-library';
import { useRouter } from 'expo-router';
import React, { useState } from 'react';
import {
    Alert,
    FlatList,
    Image,
    Linking,
    Pressable,
    StyleSheet,
    Text,
    TextInput,
    TouchableOpacity,
    View,
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

// ...

export default function Modal() {
    // ...

    const canAddThread =
        (threads.at(-1)?.text.trim().length ?? 0) > 0 ||
        (threads.at(-1)?.imageUris.length ?? 0) > 0;
    const canPost = threads.every(
        thread => thread.text.trim().length > 0 || thread.imageUris.length > 0,
    );

    const removeThread = (id: string) => {
        setThreads(prevThreads =>
            prevThreads.filter(thread => thread.id !== id),
        );
    };

    const pickImage = async (id: string) => {
        let { status } =
            await ImagePicker.requestMediaLibraryPermissionsAsync();

        if (status !== 'granted') {
            Alert.alert(
                'Permission not granted',
                'Please grant camera roll permission to use this feature',
                [
                    {
                        text: 'Open settings',
                        onPress: () => {
                            Linking.openSettings();
                        },
                    },
                    {
                        text: 'Cancel',
                    },
                ],
            );
            return;
        }

        let result = await ImagePicker.launchImageLibraryAsync({
            mediaTypes: ['images', 'livePhotos', 'videos'],
            allowsMultipleSelection: true,
            selectionLimit: 5,
        });
        console.log('image result:', result);

        if (result.canceled) return;

        setThreads(prevThreads =>
            prevThreads.map(thread =>
                thread.id === id
                    ? {
                          ...thread,
                          imageUris: thread.imageUris.concat(
                              result.assets?.map(asset => asset.uri) ?? [],
                          ),
                      }
                    : thread,
            ),
        );
    };

    const takePhoto = async (id: string) => {
        let { status } = await ImagePicker.requestCameraPermissionsAsync();
        if (status !== 'granted') {
            Alert.alert(
                'Permission not granted',
                'Please grant camera permission to use this feature',
                [
                    {
                        text: 'Open settings',
                        onPress: () => {
                            Linking.openSettings();
                        },
                    },
                    {
                        text: 'Cancel',
                    },
                ],
            );
            return;
        }

        let result = await ImagePicker.launchCameraAsync({
            mediaTypes: ['images', 'livePhotos', 'videos'],
            allowsMultipleSelection: true,
            selectionLimit: 5,
        });
        console.log('camera result:', result);

        // MediaLibrary 권한 요청 및 사진 저장
        status = (await MediaLibrary.requestPermissionsAsync()).status;
        if (status === 'granted' && result.assets?.[0].uri) {
            await MediaLibrary.saveToLibraryAsync(result.assets[0].uri);
        }

        if (result.canceled) return;

        setThreads(prevThreads =>
            prevThreads.map(thread =>
                thread.id === id
                    ? {
                          ...thread,
                          imageUris: thread.imageUris.concat(
                              result.assets?.map(asset => asset.uri) ?? [],
                          ),
                      }
                    : thread,
            ),
        );
    };

    const removeImageFromThread = (id: string, uriToRemove: string) => {
        setThreads(prevThreads =>
            prevThreads.map(thread =>
                thread.id === id
                    ? {
                          ...thread,
                          imageUris: thread.imageUris.filter(
                              uri => uri !== uriToRemove,
                          ),
                      }
                    : thread,
            ),
        );
    };

    // ...
}
{
  "name": "expo-threads-clone",
  "main": "expo-router/entry",
  "version": "1.0.0",
  "scripts": {
    "start": "expo start",
    "reset-project": "node ./scripts/reset-project.js",
    "android": "expo start --android",
    "ios": "expo start --ios",
    "web": "expo start --web",
    "lint": "expo lint"
  },
  "dependencies": {
    "@expo/vector-icons": "^15.0.2",
    "@react-navigation/bottom-tabs": "^7.4.0",
    "@react-navigation/elements": "^2.6.3",
    "@react-navigation/native": "^7.1.8",
    "expo": "~54.0.12",
    "expo-blur": "~15.0.7",
    "expo-constants": "~18.0.9",
    "expo-dev-client": "~6.0.13",
    "expo-font": "~14.0.8",
    "expo-haptics": "~15.0.7",
    "expo-image": "~3.0.8",
    "expo-image-picker": "~17.0.8",
    "expo-linking": "~8.0.8",
    "expo-location": "~19.0.7",
    "expo-media-library": "~18.2.0",
    "expo-router": "~6.0.10",
    "expo-splash-screen": "~31.0.10",
    "expo-status-bar": "~3.0.8",
    "expo-symbols": "~1.0.7",
    "expo-system-ui": "~6.0.7",
    "expo-web-browser": "~15.0.8",
    "react": "19.1.0",
    "react-dom": "19.1.0",
    "react-native": "0.81.4",
    "react-native-gesture-handler": "~2.28.0",
    "react-native-reanimated": "~4.1.1",
    "react-native-safe-area-context": "~5.6.0",
    "react-native-screens": "~4.16.0",
    "react-native-web": "~0.21.0",
    "react-native-worklets": "0.5.1"
  },
  "devDependencies": {
    "@types/react": "~19.1.0",
    "typescript": "~5.9.2",
    "eslint": "^9.25.0",
    "eslint-config-expo": "~10.0.0"
  },
  "private": true
}

답변 1

1

제로초(조현영)님의 프로필 이미지
제로초(조현영)
지식공유자

{"expo":{"plugins":[["expo-media-library",{"photosPermission":"Allow $(PRODUCT_NAME) to access your photos.","savePhotosPermission":"Allow $(PRODUCT_NAME) to save photos.","isAccessMediaLocationEnabled":true,"granularPermissions":["audio","photo"]}]]}}

granularPermissions를 하면 원래 되어야 합니다. 빌드를 다시 해야하는것으로 보이는데 expo go 지웠다가 다시 실행해보시겠어요?

Sckroll님의 프로필 이미지
Sckroll
질문자

해결했습니다!

Expo Go 지웠다가 다시 설치하면서 확인해봤는데, 처음부터 Development build가 아닌 Expo Go에서 실행하고 있었던 것이 원인이었습니다... 네이티브 모듈은 Expo Go에서 사용할 수 없다는 걸 생각하지 못했네요 ㅜ Development build 전환 후 테스트해보니 잘 동작하는 걸 확인했습니다. 감사합니다!

  • Before

    image.png
  • After

    image.png

     

Sckroll님의 프로필 이미지
Sckroll

작성한 질문수

질문하기