[Android] API통신을 위한 비동기 처리 feat. Coroutine

jsieon97·2023년 5월 24일
0

Android

목록 보기
1/1

안드로이드에서의 비동기 처리

Kotlin을 활용한 안드로이드 앱을 만드는 프로젝트를 진행 중 REST API 통신이 필요하게 되었고 다음과 같이 코드를 작성했다.

API 요청 함수 (Retrofit 사용)

interface PictureApiService {

	...
   
   	// 오늘의 이미지 리스트를 받아오는 요청
    @GET("/picture/today_list/no_user")
    suspend fun getTodayImages(): List<PictureResponse>
    
    ...
    
}
// PictureApiService
// retrofit 요청 생성 함수
fun PictureApiService(): PictureApiService {
    val retrofit = Retrofit.Builder()
        .baseUrl(BuildConfig.API_SERVER)
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    return retrofit.create(PictureApiService::class.java)
}

위 함수를 활용하기 위해 Composable 함수에서 API를 요청해보았다.

요청 함수 활용

val response = PictureApiService().getTodayImages()

위와 같이 response에 데이터를 담으려 하면 다음과 같은 오류가 발생했다.

Suspend function 'getTodayImages' should be called only from a coroutine or another suspend function

해석해보면 일시 중단 함수(suspend fun)는 코루틴 혹은 다른 일시중단 기능에서 활용해야 한다는 의미이다.

API 요청을 일시중단 함수로 하지않으면? 네트워크 IO 요청이 메인스레드에서 처리되어 NetworkOnMainThreadException가 발생한다.

해결 방안

LaunchEffect 활용

LaunchedEffect(Unit) {
        val response = PictureApiService().getTodayImages()
    }

위와 같이 작성하면 오류 없이 실행할 수 있다 또한 Unit 자리에 변수를 넣어 변수 값이 변환 될 때마다. 함수를 실행하도록 하여 UI를 좀 더 유연하게 만들 수 있다.

예를 들어 아래와 같이 작성하면 tag값에 변화에 따라 요청이 달라지도록 하여 posts값을 바꿔 화면에 표시되는 값을 변환할 수 있다.

LaunchedEffect(tag) {
	try {
    	isLoading = true
        val response = if (currentUser != null) {
       		val token = currentUser.getIdToken(false).result?.token.toString()
       		val uid = currentUser.uid
        	when (tag) {
        		"Today's Pick" -> pictureApiService.getUserTodayImages(token, uid)
        		"Weekly" -> pictureApiService.getUserWeeklyImages(token, uid)
        		"Monthly" -> pictureApiService.getUserMonthlyImages(token, uid)
        		else -> pictureApiService.getUserTagImages(token, tag, uid)
        	}
    	} else {
        	when (tag) {
	        	"Today's Pick" -> pictureApiService.getTodayImages()
	        	"Weekly" -> pictureApiService.getNoUserWeeklyImages()
        		"Monthly" -> pictureApiService.getNoUserMonthlyImages()
            	else -> pictureApiService.getNoUserTagImages(tag)
            }
        }
        posts = response
    } catch (e: Exception) {
    	e.printStackTrace()
        posts = emptyList()
    } finally {
        isLoading = false
    }
}

rememberCoroutineScope 활용

val coroutineScope = rememberCoroutineScope()

	...   
    
    onClick = {
    	coroutineScope.launch {
        	// Coroutine 필요 요청들 
            PictureApiService().getTodayImages()
        }
    }
    
    ...

이번 프로젝트를 진행하면서 위 두 가지 방법을 가장 많이 사용했는데 두 방법의 차이점은

  • LaunchEffect
    • 컴포저블이 처음 실행/다시 실행될 때(또는 키 매개변수가 변경되었을 때) 어떤 조치를 취해야 하는 경우에 사용
  • rememberCoroutineScope
    • 컴포저블 외부에 있지만 컴포지션을 종료한 후 자동으로 취소되도록 범위가 지정된 코루틴을 실행하려는 경우에 사용
    • LaunchEffect에서 rememberCoroutineScope을 활용할 수도 있다.

전체 코드

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Tab(scrollBehavior: TopAppBarScrollBehavior, context: Context, tag: String) {
    // 현재 화면 구성에 대한 정보를 가져옴
    val configuration = LocalConfiguration.current
    // 화면의 너비(DP)를 가져옴
    val screenWidth = configuration.screenWidthDp.dp

    // 목록 상태를 기억하며 코루틴 스코프를 생성
    val listState = rememberLazyGridState()
    val coroutineScope = rememberCoroutineScope()

    // 로딩 상태를 기억
    var isLoading by remember { mutableStateOf(false) }

    // 사진 API 서비스 객체를 생성
    val pictureApiService = PictureApiService()

    // 현재 사용자 정보를 가져옴
    val currentUser = FirebaseAuth.getInstance().currentUser

    // 게시물 목록 상태를 기억
    var posts by remember { mutableStateOf(emptyList<PictureResponse>()) }
    // 새로 고침 상태를 기억
    var isRefreshing = remember { mutableStateOf(false) }

    // 게시물을 가져오는 코루틴 함수
    val fetchPosts = suspend {
        try {
            isLoading = true
            val response = if (currentUser != null) {
                val token = currentUser.getIdToken(false).result?.token.toString()
                val uid = currentUser.uid
                // 사용자Images(token, uid)
                    "Weekly" -> pictureApiService.getUserWeeklyImages(token, uid)
                    "Monthly" -> pictureApiService.getUserMonthlyImages(token, uid)
                    else -> pictureApiService.getUserTagImages(token, tag, uid)
                }
            } else {
                when (tag) {
                    "Today's Pick" -> pictureApiService.getTodayImages()
                    "Weekly" -> pictureApiService.getNoUserWeeklyImages()
                    "Monthly" -> pictureApiService.getNoUserMonthlyImages()
                    else -> pictureApiService.getNoUserTagImages(tag)
                }
            }
            posts = response
        } catch (e: Exception) {
            e.printStackTrace()
            posts = emptyList()
        } finally {
            isLoading = false
        }
    }

    // 태그 값이 변경될 때마다 게시물 목록을 가져옴
    LaunchedEffect(tag) {
        coroutineScope.launch {
            fetchPosts()
        }
    }

    // 스와이프 새로고침 컴포넌트의 상태를 기억
    val swipeRefreshState = rememberSwipeRefreshState(isRefreshing.value)

    SwipeRefresh 로그인 여부에 따라 태그에 맞는 이미지 목록을 가져옴
                when (tag) {
                    "Today's Pick" -> pictureApiService.getUser        state = swipeRefreshState,
        onRefresh = {
            // 새로고침 이벤트 발생 시 게시물을 다시 가져옴
            coroutineScope.launch {
                isRefreshing.value = true
                fetchPosts()
                isRefreshing.value = false
            }
        },
        indicator = { state, trigger ->
            SwipeRefreshIndicator(
                state = state,
                refreshTriggerDistance = trigger,
                contentColor = MaterialTheme.colors.primary
            )
        }
    ) {
        // 로딩 중일 때 로딩 인디케이터를 표시하고, 아닐 경우 게시물 목록을 표시
        if (isLoading) {
            Box(
                modifier = Modifier.fillMaxSize(),
                contentAlignment = Alignment.Center
            ) {
                CircularProgressIndicator()
            }
        } else {
            LazyVerticalGrid(
                modifier = Modifier,
                columns = GridCells.Adaptive(minSize = screenWidth / 2 - 4.dp),
                state = listState,
                contentPadding = PaddingValues(2.dp),
            ) {
                itemsIndexed(posts) { index, post ->
                    // 각 게시물에 대해 PostListItem 컴포넌트를 생성
                    PostListItem(post = post, context = context, screenWidth = screenWidth)
                }
            }
        }
    }
}
참고 사이트
두 가지 방법의 차이점

https://stackoverflow.com/questions/66474049/using-remembercoroutinescope-vs-launchedeffect

Compose의 코루틴

https://developer.android.com/jetpack/compose/side-effects?hl=ko

profile
개발자로써 성장하는 방법

0개의 댓글