google play api는 google play service의 하나의 파트 임.

google fit api는 안드로이드 4.1 이상부터 호환.

Google Fit API 장점

  • 거의 실시간의 히스토리 데이터를 적은 에너지의 블루투스 디바이스로부터 가져옴
  • 활동들을 기록 할 수 있음
  • 데이터를 세션과 연관시킬 수 있음
  • 피트니스 목표를 설정 할 수 있음

sensor data

  • 유저의 하루 활동 관련한 정보들을 앱에서 제공한다면 (예를 들어 하루동안의 걸음수), sensor data는 사용자의 행동을 거의 실시간으로 보여주는데 사용이 가능함.

record data

  • 앱이 꺼져있는 상황에서도 계속해서 데이터를 적재 할 수 있는 subscribe 메서드를 제공함

historical data

  • 만약 유저가 과거의 활동들로부터 피트니스 데이터를 보여주기를 원한다면 history api를 사용하면 됨
  • 데이터 subscribe가 선행 되어야 historical data를 읽을 수 있음 ( 도큐먼트가 친절하지 않아서 테스트 후에 알게 됨), 데이터가 없으면 빈 리스트로 응답이 옴.

session data

  • 개발자가 일정한 시간의 세션을 설정하고 그 설정안에 들어가는 범위의 데이터를 가져올 수 있음

내가 하고자 하는 것

  1. subscribe를 통해 앱 사용자의 활동내역(현재는 걸음 수, 이후에 추가할 예정)들을 클라우드 형태의 google fit store에 앱을 종료했을 때도 마찬가지로 계속해서 저장해야 함.
  2. 데이터가 있는 날부터 일(00시~24시) 기준으로 걸음 수 데이터를 보여줄 수 있어야 함
  3. 배터리 최적화, 잠자기 모드 등 예기치 못한 상황에서도 정상적으로 데이터를 가져와야 함
  4. 데이터는 자정 기준으로 새로 시작함

사전 준비

  1. google fit api 는 Google API Console 에 프로젝트를 등록 및 client ID 를 발급 받은 후 사용해야함.
  2. client ID 발급 후 사용할 library에서 google fit api 를 사용 설정 해야 함.
  3. 테스트를 위해서 OAuth 동의 화면 탭의 테스트 사용자에 테스트를 수행 할 사용자 정보를 등록해야 함.
  4. 모듈 범위의 gradle 에
plugin {
    id("com.android.application")
}

...

dependencies {
        implementation("com.google.android.gms:play-services-fitness:21.1.0")
        implementation("com.google.android.gms:play-services-auth:20.2.0")
}

다음 과 같이 dependency를 추가하여 필요한 라이브러리를 다운로드 함

  1. https://developers.google.com/fit/android/get-started 를 참고하면 더 자세히 setup에 관해 나와 있음

구현 방법

  1. fitness option 객체를 만들어 내가 사용하고자 하는 data들을 추가함
funcreateFitnessStepOptions() : FitnessOptions{
	val fitnessOptions = FitnessOptions.builder()
        .addDataType(DataType.TYPE_STEP_COUNT_DELTA, FitnessOptions.ACCESS_READ)
        .addDataType(DataType.AGGREGATE_STEP_COUNT_DELTA, FitnessOptions.ACCESS_READ)
        .addDataType(DataType.TYPE_DISTANCE_DELTA, FitnessOptions.ACCESS_READ)
        .addDataType(DataType.AGGREGATE_DISTANCE_DELTA, FitnessOptions.ACCESS_READ)
        .build()
	return fitnessOptions
}

걸음수와 지금까지 걸은 거리에 대한 데이터를 얻기 위해 위처럼 4개의 fitness data type을 추가함.

  1. google account를 가져옴
fun getAccount(fitnessOptions: FitnessOptions) : GoogleSignInAccount{
	val account = GoogleSignIn.getAccountForExtension(GlobalApplication.getApplicationContext(),fitnessOptions)
	return account
}

getAccountForExtension 메서드를 이용하여 추가한 데이터에 대한 인증을 사용하는 google signin account를 얻음.

  1. google fit api를 사용하기 위한 권한 요청
fun checkHasGrantedDataAccess(account: GoogleSignInAccount, fitnessOptions: FitnessOptions, callback: () -> Unit){
    Log.i(TAG,"checkHasGrantedDataAccess${account.id}, fitnessOptions${fitnessOptions}")
if(!GoogleSignIn.hasPermissions(account, fitnessOptions)){
	//해당 google id가 permission이 허용되었는지 체크함.
	Log.d(TAG,"!GoogleSignIn.hasPermissions")
	GoogleSignIn.requestPermissions(mActivity,GOOGLE_FIT_PERMISSIONS_REQUEST_CODE, account, fitnessOptions)
  }else{
	  callback()
  }
}

account 가 접근하고자 하는 google fit api data에 대한 권한이 있는지 체크하고, 권한이 있다면 callback함수를 실행함. 없다면 권한을 요청함.

  1. 걸음 수 / 걸은 거리 데이터 구독 시작
//피트니스 데이터(걸음 수)구독셋팅
fun subscribeStepCountData(successCallback: () -> Unit?, failureCallback: () -> Unit?){
    Fitness.getRecordingClient(GlobalApplication.getApplicationContext(),mGoogleSignInAccount)
  .subscribe(DataType.TYPE_STEP_COUNT_DELTA)// no scopes are specified.
	.addOnSuccessListener{
	Log.i(TAG,"successfully subscribe step count data!!")
	  successCallback()
	}.addOnFailureListener{
	Log.w(TAG,"there was a problem subscribing step count data${it}")
	  failureCallback()
	}
}

  1. 걸음 수를 원하는 날짜부터 가져옴 (수정 중)
fun getStepCountForDays(days : Int){

	val cal = Calendar.getInstance()
	val now = Date()
	    cal.time= now
	val endTime = cal.timeInMillis
	
	cal.set(Calendar.HOUR_OF_DAY, -days)
	    cal.set(Calendar.MINUTE, 0)
	    cal.set(Calendar.SECOND, 0)
	    cal.set(Calendar.MILLISECOND, 0)
	
	val startTime = cal.timeInMillis
	
	val readRequest = DataReadRequest.Builder()
	        .aggregate(DataType.TYPE_STEP_COUNT_DELTA, DataType.AGGREGATE_STEP_COUNT_DELTA)
	        .bucketByTime(1, TimeUnit.DAYS)
	        .setTimeRange(startTime, endTime, TimeUnit.SECONDS)
	        .build()
	
	    Fitness.getHistoryClient(mActivity,mGoogleSignInAccount)
	    .readData(readRequest)
	    .addOnSuccessListener{
	response->
	for(dataSetinresponse.buckets.flatMap{ it.dataSets}) {
	  Log.i(TAG,"dataSet ==${dataSet}")
		//원하는 로직 추가 }
	}.addOnFailureListener{
		//실패 처리로직 추가
	}
}

원하는 날부터 현재까지 일 단위의 걸음 수 데이터를 가져오는 로직

결과

테스트를 해보려고 구글 피트니스 앱과 데이터를 비교한 결과 데이터가 잘 조회 되는 것을 확인하였음. 이 과정에서 한번 더 고민하고 액션을 취했던 부분은 혹시나 하는 마음에 매일 자정이 되기 찰나의 순간에 하루동안의 데이터를 조회해 오는 메서드 readDailyTotal 를 호출하여 데이터를 저장하는 방법과, DataReadRequest 객체에 startTime과 endTime을 설정해 그 기간동안의 data를 가져와 사용하는 방법 간 데이터 결과에 차이가 있는가에 대한 테스트를 했고 데이터간 차이가 없는 것을 확인하고 후자의 방식으로 채택했음. 그러나 startTime과 endTime을 잘못 설정하면 자정이 기준이 아니고 현재 시간을 기준으로 하루가 책정되기 때문에 startTime과 endTime을 설정을 자신이 필요한 데이터에 따라 정확히 할 필요가 있음.

Chronometer를 이용한 스톱워치 구현

<Chronometer
android:id="@+id/timer"
android:layout_width="200dp"
android:layout_height="40dp"
android:textSize="16sp"/>

xml파일에 다음과 같이 Chronometer 를 추가함.

나는 start, pause, stop 버튼에 각각 시작, 일시정지, 중지 기능을 추가해 넣었음.

전역변수로는 일시 정지를 누른 시간을 저장함

fun startTimer(){
	timer.base= SystemClock.elapsedRealtime() +pauseTime
  timer.start()
}

fun pauseTimer() {
	pauseTime=timer.base- SystemClock.elapsedRealtime()
	timer.stop()
}

fun resetTimer(){
	pauseTime= 0L
	timer.stop()
}

각각 버튼의 클릭리스너에 기능에 맞는 함수를 호출하면 됨.

여기서 timer는 Chronometer 임**.**

1. 하고자 하는 기능?

사용자가 자신이 갔던 경로를 트랙킹하여 지도에 표시해 타 사용자들에게 공유하는 기능

2. 프로세스

  1. 해당 화면 접근 시 런타임에 위치 권한 체크
  2. 위치 권한 허용 시 (3)으로 이동, 거부시 (5)로이동
  3. start 버튼 선택 시 현재 내 위치로 이동 및 tracking시작
  4. 설정한 시간 간격으로 위치를 얻어와 지도 위에 선으로 표시
  5. stop버튼 선택 시 tracking 종료
  6. 위치 권한 재 요청, 2번 거부 시 수동으로 앱 설정에 가서 허용하라는 다이얼로그 표시

3. 구현 방법

3-1) 런타임 위치 권한 체크

** 위치 권한 체크 시 유의할 점

내가 구현하고자 했던 기능은 앱이 백그라운드 상태에 가 있을 때에도 사용자의 위치를 추적해야 했다. 안드로이드 6부터는 앱에서 필요한 권한이 있을 때 런타임에서 권한을 받게 되었는데, 위치 권한은

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

manifest.xml

ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION 두개의 권한을 받아 사용하였다. 첫번째 줄에 선언한 권한은 네트워크 만을 이용하여 대략적인 위치 정보를 요청하는 권한이고 두번째 줄에 선언한 권한은 GPS와 네트워크를 이용하여 정확한 위치 정보를 요청하는 권한이다. ACCESS_FINE_LOCATION 권한은 반드시 ACCESS_COARSE_LOCATION권한이 허용되어야 한다.

백그라운드에서의 위치 권한은 안드로이드 10 미만으로는 따로 선언하지 않고 사용할 수 있다.

안드로이드 10 이상 부터는 위치 정보 사용이 포그라운드/ 백그라운드로 나누어지게 되는데 백그라운드에서 위치 권한을 사용하려면

<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />

manifest.xml

ACCESS_BACKGROUND_LOCATION 권한을 따로 요청해야한다.

안드로이드 11이상 부터는 백그라운드에서 위치정보 사용 시 런타임 권한을 두 번 요청 해야한다.

포그라운드/ 백그라운드 위치 권한을 동시에 받으면 제대로 권한 체크가 되지 않고 무시하게 된다.

백그라운드 위치 권한은 왜 백그라운드 위치 권한을 받는지에 대한 다이얼로그와 함께 백그라운드 위치 권한을 사용자가 허용으로 설정할 수 있도록 설정 페이지로 보내주어야 하며 사용자 선택에 따른 결과 처리도 알맞게 따로 해주어야 한다.

3-2) 좌표 값 얻기

나는 Google Play 서비스 Location API를 사용하여 일정한 시간 주기로 좌표 값을 가져왔다. FusedLocationProviderClient가 제공하는 getCurrentLocation 메서드를 이용하여 현재 위치 값을 가져왔다. 문서에는 getLastLocation메서드를 사용해 좌표값을 가져오는 것을 권장한다고 적혀있지만 이전 프로젝트에서 사용했을 때 위치 설정을 막아 놓았다가 막 켠 상태라면 저장되어 있는 좌표 값이 없어 좌표가 null로 반환되는 오류가 있었다. 이 때문에 지속적으로 위치 정보를 가져오지 않고 한번만 가져와도 무방하다면 getCurrentLocation 메서드를 사용하는게 더 좋다고 생각한다.

계속해서 업데이트 되는 좌표를 얻기 위해서는 requestLocationUpdates 메서드를 사용하여 지속적으로 위치를 업데이트 할 수 있다. 위치 정보를 가져올 시간 간격, 정확도를 설정해 LocationRequest객체를 생성하여 설정한 간격으로 좌표값을 가져온다.

directory structure

.nuxt

개발 모드에서 vue application을 만들기 위한 directory

.output

배포 모드에서 vue application을 만들기 위한 directory

assets

프로젝트에 사용되는 모든 assets을 포함하며 빌드 도구인 webpack, vite의 영향을 받는다.

  • Stylesheets (CSS, SASS, etc.)
  • Fonts
  • Images that won’t be served from the public/ directory

만약 assets을 서버에서 가져오고 싶다면 public 루트에 넣는 것이 좋음.

components

컴포넌트들을 넣어두는 디렉토리. 컴포넌트는 pages 안에 있는 vue 페이지나 component에서 import 될 수 있음. Nuxt는 components 디렉토리에 있는 파일을 자동으로 import 해줌.

Component Names

| components/
--| base/
----| foo/
------| Button.vue

만약 다음과 같이 중첩되는 디렉토리를 가졌다면

컴포넌트 이름은 <BaseFooButton /> 가 된다.

Dynamic components

resolveComponent 를 사용해서 동적으로 빌트인 컴포넌트를 이용하여 component를 import 할 수 있다.

<template>
	<component :is="clickable ? MyButton : 'div'" />
</template>
<script setup>
	const MyButton = resolveComponent('MyButton')
</script>

Dynamic imports

컴포넌트를 동적으로 import하기 위해서는 component이름 앞에 Lazy를 붙임

컴포넌트가 항상 필요하지 않을 경우에는 동적으로 맞는 타이밍에 import 시킴으로써 번들 사이즈를 최적화 할 수 있음.

Library Authors

사용할 라이브러리의 자동 import와 tree-shaking이 쉬움

node_modules안에 있는 각 라이브러리 디렉토리에 nuxt.js파일을 만들고 components:dirs 훅에서 control 할 수 있음.

nuxt.config에서

export default {
  modules: ['awesome-ui/nuxt']
}

다음과 같이 modules로 라이브러리를 추가 할 수 있음.

composables

Nuxt는 vue의 composable을 auto import 함.

예를들어

composables
 | - useFoo.ts // scanned
 | - useBar
 | --- supportingFile.ts // not scanned
 | --- index.ts // scanned

위와 같은 디렉토리 구조에서 useFoo.ts 와 useBar/index.ts 는 자동으로 import된다. 특히 useBar/index.ts 는 index로 등록 되지 않고 userBar로 등록된다.

export const useBar = () => {
}

// Enables auto import for this export
export { useBaz } from './supportingFile'

만약 useBar/supportingFile.ts 를 자동으로 import하고 싶을 때는 위와 같이 useBar/index.ts에서 userBar/supportingFile.ts를 다시 한번 export 시켜줘야 한다.

layouts

Nuxt는 커스텀가능한 layout을 제공하는데

만약 예를들어 pages 디렉토리가 아닌 app.vue 하나에서 사용 한다면

-| layouts/
---| custom.vue
-| app.vue

라는 디렉토리 구조에서

custom.vue는

<template>
	<div>
    Some shared layout content:
    <slot />
	</div>
</template>

다음과 같이 보여줄 페이지 자리에 <slot/>을 입력함

<template><NuxtLayout name="custom">
    Hello world!
  </NuxtLayout></template>

app.vue에서 layout을 다음과 같이 사용함

만약 /pages 디렉토리를 사용한다면

-| layouts/
---| custom.vue
-| pages/
---| index.vue

다음과 같은 디렉토리 구조에서

<script>
// This will also work in `<script setup>`
definePageMeta({
  layout: "custom",
});
</script>

index.vue 파일에 definePageMeta 메서드를 이용하여 layout을 지정해 사용할 수 있다.

middleware

Nuxt는 커스텀 가능한 route middleware를 제공함. 이는 global하게 앱 전역에서 사용이 가능함

지정한 route에 navigate 할 때 code를 작성할 수 있음.

pages

Usage

Nuxt는 vue router를 이용해 file-based 라우팅을 제공함

page와 component는 .vue .js .jsx .ts .tsx 확장자를 가질 수 있음

pages/index.vue

<template><h1>Index page</h1></template>

pages/index.ts

// <https://vuejs.org/guide/extras/render-function.html>
export default defineComponent({
  render () {
    return h('h1', 'Index page')
  }
})

pages/index.tsx

// <https://vuejs.org/guide/extras/render-function.html#jsx-tsx>
export default defineComponent({
  render () {
    return

Index page

  }
})

pages 디렉토리 안 index.vue 는 / route와 매핑됨

transition을 이용하려면 무조건 하나의 root element를 가져야 함

⇒ server-rendered 된 페이지는 불가능

definePageMeta({
  title,  // This will create an error
  someData
})

으로 router의 meta data를 지정할 수 있음

특별한 Metadata로는 keepalive, key, layout, middleware, layoutTransition, pageTransition, alias 등이 있음

Navigation

두 페이지 간 전환이 필요할 때 <NuxtLink> 컴포넌트를 이용함.

NuxtLink 컴포넌트는 Nuxt에 기본으로 포함되어 있어 import할 필요 없음

<template><NuxtLink to="/">Home page</NuxtLink></template>

위와 같이 to=”/path” 형식으로 라우팅함

Programmatic Navigation

navigateTo() 메서드를 이용하여 프로그래밍적으로 라우팅이 가능함

<script setup>
const router = useRouter();
const name = ref('');
const type = ref(1);

function navigate(){
  return navigateTo({
    path: '/search',
    query: {
      name: name.value,
      type: type.value
    }
  })
}
</script>

plugins

Nuxt는 plugins 디렉토리의 plugin 파일들을 자동으로 읽음

.server .client 같은 파일 뒤 suffix로 서버사이드 렌더링이나 클라이언트 사이드 렌더링에서 사용되는 플러그인을 구분 할 수 있음

plugins/ directory의 가장 상위 레벨의 파일들이 plugin으로 등록됨

예를들어

plugins
 | - myPlugin.ts
 | - myOtherPlugin
 | --- supportingFile.ts
 | --- componentToRegister.vue
 | --- index.ts

에서 myPlugin.ts 와 myOtherPlugin/index.ts 가 등록됨

Creating plugins

export default defineNuxtPlugin(nuxtApp => {
  // Doing something with nuxtApp
})

plugin을 정의 할때 받는 매개변수는 nuxtApp 하나임

Automatically providing helpers

export default defineNuxtPlugin(() => {
  return {
    provide: {
      hello: () => 'world'
    }
  }
})

위와 같이 Nuxtapp 인스턴스에 helper를 제공하기 위해서는 provide key 밑 property로 설정 후 provide를 return 함

<template><div>
    {{ $hello() }}
  </div></template><script setup lang="ts">
// alternatively, you can also use it here
const { $hello } = useNuxtApp()
</script>

vue 파일에선 위와 같이 사용함

코루틴

코루틴 이란 ? 비동기적으로 실행되는 코드를 간소화 하기 위해 안드로이드에서 사용할 수 있는 동시 실행 설계 패턴입니다.

  • 경량 : 코루틴은 실행중인 스레드를 차단하지 않는 정지를 지원하므로 단일 스레드에서 많은 코루틴을 실행할 수 있습니다.
  • 메모리 누수 감소 : 구조화된 동시 실행을 사용하여 범위 내에서 작업을 실행합니다.
  • 기본으로 제공되는 취소 지원 : 실행 중인 코루틴 계층 구조를 통해 취소가 전달됩니다.
  • 이외 Jetpack 지원등..

백그라운드 스레드에서 실행

기본 스레드에서 네트워크 요청을 보내면 응답을 받을 때까지 스레드가 대기하거나 차단됩니다.

스레드가 차단될 경우 앱이 정지되고 응답없음(ANR) 대화상자가 표시될 수 있습니다.

이것의 해결방식으로 네트워크 요청을 새로운 코루틴을 만들고 I/O스레드에서 네트워크 요청을 실행할 수 있습니다.

example 1)

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun login(username: String, token: String) {
        // Create a new coroutine to move the execution off the UI thread
        viewModelScope.launch(Dispatchers.IO) {
            val jsonBody = "{ username: \\"$username\\", token: \\"$token\\"}"
            loginRepository.makeLoginRequest(jsonBody)
        }
    }
}
  • viewModelScope는 사전 정의된 CoroutineScope 입니다. 모든 코루틴은 코루틴범위 내에서 실행해야 합니다. CoroutineScope는 하나 이상의 관련 코루틴을 관리합니다.
  • launch는 코루틴을 만들고 함수 본문의 실행을 해당하는 디스패처에 전달하는 함수입니다.
  • Dispatchers.IO는 이 코루틴을 I/O작업용으로 예약된 스레드에서 해야 함을 나타냅니다.

기본 안전을 위해 코루틴 사용

기본 스레드에서 UI 업데이트를 차단하지 않는 한수를 기본 안전 함수로 간주합니다. 기본스레드에서 네트워크 요청을 하면 UI가 차단되므로 이 네트워크 요청이 담긴 함수는 기본 안전 함수가 아닙니다.

코루틴 라이브러리의 withContext() 함수를 사용하여 코루틴 실행을 다른 스레드로 이동합니다.

example 2)

class LoginRepository(...) {
    ...
    suspend fun makeLoginRequest(
        jsonBody: String
    ): Result<LoginResponse> {

        // Move the execution of the coroutine to the I/O dispatcher
        return withContext(Dispatchers.IO) {
            // Blocking network request code
        }
    }
}
  • withContext(Dispatcher.IO)는 코루틴 실행을 I/O스레드로 이동하여 호출 함수를 기본 안전 함수로 만들고 필요에 따라 UI를 업데이트 하도록 설정합니다.
  • makeLoginRequest 에는 supsend 키워드가 표시됩니다. suspend키워드는 코루틴 내에서 함수가 호출되도록 강제하는 Kotlin의 방법입니다.

example 3)

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun login(username: String, token: String) {

        // Create a new coroutine on the UI thread
        viewModelScope.launch {
            val jsonBody = "{ username: \\"$username\\", token: \\"$token\\"}"

            // Make the network call and suspend execution until it finishes
            val result = loginRepository.makeLoginRequest(jsonBody)

            // Display result of the network request to the user
            when (result) {
                is Result.Success<LoginResponse> -> // Happy path
                else -> // Show error in UI
            }
        }
    }
}

다시 example 1을 수정한 버전을 살펴봅시다.

  • launch가 Dispatchers.IO 매개변수를 사용하지 않습니다. Dispatcher를 launch에 전달하지 않으면 코루틴은 기본 스레드에서 실행됩니다.
  • 앱이 기본스레드의 View 레이어에서 login 함수를 호출합니다.
  • launch가 기본 스레드에서 네트워크 요청을 보낼 때 코루틴을 만들며, 코루틴이 실행을 시작합니다.
  • 코루틴 내에서 makeLoginRequest 호출은 withContext 블록 실행이 끝날 때 까지 코루틴의 추가 실행을 정지합니다 (ViewmodelScope의 코루틴의 코드를 makeLoginRequest의 withContext 블록 실행이 끝날때까지 일시 정지하고 끝난 후 부터 다시 실행합니다.)
  • withContext블록이 완료되면 네트워크 요청 결과와 함께 기본 스레드에서 실행을 재개합니다.

 

출처 : https://developer.android.com/kotlin/coroutines?hl=ko 

 

Android의 Kotlin 코루틴  |  Android 개발자  |  Android Developers

Android의 Kotlin 코루틴 코루틴은 비동기적으로 실행되는 코드를 간소화하기 위해 Android에서 사용할 수 있는 동시 실행 설계 패턴입니다. 코루틴은 Kotlin 버전 1.3에 추가되었으며 다른 언어에서 확

developer.android.com

 

현재 상황

현재 vue.js를 이용한 SPA을 만들어 배포하고 있는데, Code Splitting을 적용한 이후로 새로 앱을 배포할 때

ChunkLoadError라는 에러를 만나게 되었다..

배포를 해야 에러를 테스트 할 수 있기 때문에 테스트도 굉장히 힘들었는데

이 에러를 해결한 과정을 정리하여 써보려고 한다.

 

 

Code Splitting이란?

webpack을 이용해 번들링을 하면, 초기 로딩시 번들링을 통해 합쳐진 js파일을 로딩하기 때문에 앱이 점점 커지면서 초기로딩이 길어지는 문제가 생긴다.

이를 해결하기 위한 것이 바로 Code Splitting, Lazy Loading이다.

 

문제점?

사용자가 앱을 사용하는 도중에 웹앱 재배포가 일어날 때, index.html은 새로 배포된 파일이 아닌 이전 파일의 정보를 담고 있기 때문에 배포 이전 파일들을 요청하게 되어 이 파일들을 찾지 못하는 상황이 일어난다. (파일은 새롭게 배포되어있는 상태이기 때문에 이전 파일들은 찾아도 찾을 수 없게 된다)

 

디버깅 테스트 과정

우리 프로젝트는 vue2+ vue router + vue CLI + webpack 4 로 구성되어 있는 상태였다.

테스트 과정에서 새롭게 안 사실은 개발모드로 빌드를 한 파일들은 파일 끝에 해시값이 붙지 않아서 위와 같은 오류가 애초에 발생하지 않는 다는 사실이다. 그래서 개발환경에서 앱을 재배포하는 과정에서는 발견되지 않은 에러가 운영 환경에서는 발견 되었던 것이다.

 

이와 같은 사실은, 개발환경에서 아이폰에서 웹앱 개발시 앱을 껐다가 켜도 계속해서 변경사항이 반영되지 않는 현상도 설명이 가능했다. 개발버전으로 빌드 시 청크파일에 해시값이 애초에 붙지 않기 때문에 이전에 배포된 파일의 이름과 새롭게 빌드되어 배포된 파일의 이름이 같아 새롭게 index.html을 배포해도 브라우저에 캐시된 새로 배포되기 이전 파일을 가져오기 때문이었다. 

 

production 모드로 빌드를 진행하면 각 chunk파일 이름 뒤에 랜덤의 해시 값이 붙게 되는데, 해시값 설정은 webpack의 config 파일에 여러가지 변경이 가능하다.

 

우리는 vue CLI를 이용하고 있었기 때문에 vue.config.js 파일의 웹팩 설정 밑에

 output: {
            filename: `[name].[chunkhash].js`,
            chunkFilename: `[name].[chunkhash].js`,
        },
 
을 추가하여 해시값을 개발모드에서도 붙여주었다.
 
참고로 해시값은 변경된 파일에 한해서만 변경이 되고, 만약 그 파일에 변경사항이 없다면 해시값을 유지함으로써 캐싱된 화면에 대해서 새롭게 로딩을 할 필요성을 줄여준다.
 
테스트는 
 
1.앱을 실행한다.

2. 특정 화면에 해당하는 파일에 변경사항을 추가한다.

3. 앱을 재 배포한다.

4. 변경사항을 추가했던 화면에 접근한다.
 
순으로 진행 되었고, 드디어 개발 모드에서 ChunkError를 만나게 될 수 있었다
 

자바스크립트에서 event를 컨트롤 할 때 preventDefault라는 메서드를 자주 접하게 된다.

 

event.preventDefault()는 해당 event가  cancelable 할때 (cancelable이 true일 때) 해당 이벤트의 기본 동작의 수행을 막는 메서드이다. 모든 event가 cancelable 하지는 않으니 모든 이벤트에 유효한 메서드는 아니다. cancelable 하지 않은 이벤트는 preventDefault를 호출해도 아무 반응이 일어나지 않는다.

 

 

예를 들어서 체크박스의 클릭이벤트를 막아 체크박스를 체크하지 못하게 만든다거나, submit 이벤트에 사용하여 데이터를 전송하지 못하게 하는 등 다양한 방면에서 사용 가능하다.

 

그러나 preventDefault는 이벤트 전파를 막지는 못합니다. 상위요소로의 이벤트 전파를 막기 위해서는 stopPropagation() 또는 stopImmediatePropagation()을 함께 사용해야합니다.

 

참고

https://developer.mozilla.org/ko/docs/Web/API/Event/preventDefault

 

Event.preventDefault() - Web API | MDN

Event 인터페이스의 preventDefault() 메서드는 어떤 이벤트를 명시적으로 처리하지 않은 경우, 해당 이벤트에 대한 사용자 에이전트의 기본 동작을 실행하지 않도록 지정합니다.

developer.mozilla.org

 

맞닥드린 문제

최근 구글 플레이스토어 HTML 태그를 파싱해서 현재 스토어에 올라가있는 앱 버전을 가지고와 업데이트 체크를 하는 로직이 구글측에서 태그를 변경하면서 사용할 수 없게 되자 RemoteConfig로 config값을 옮기는 과정이 있었다. 그러던 중 업데이트 버전을 받아오는 로직을 독립적인 클래스의 함수로 만들어 의존성을 줄이고자 하였는데, listener안에서 적시에 받아온 값을 어떻게 사용하느냐에 대한 문제가 생겼다. 

 

생각해본 해결방법

매개변수에 함수를 전달하여 실행시키면 어떨까? 라는 생각이 들어 검색해봤고, 코틀린은 고차함수와 람다를 제공함을 알아내어 적용시켜보았다. 고차함수란 일반함수에 또다른 함수를 인자나 반환값으로 사용하는 함수이다. 

사용법과 설명은 https://play.kotlinlang.org/byExample/04_functional/01_Higher-Order%20Functions

 

Kotlin Playground: Edit, Run, Share Kotlin Code Online

 

play.kotlinlang.org

https://kotlinlang.org/docs/lambdas.html#function-literals-with-receiver

 

High-order functions and lambdas | Kotlin

 

kotlinlang.org

를 참고하면 좋다.

 

해결?

나는 인자에 () -> Unit 형의 함수를 넣어 원하는 곳에서 실행시키는 방식으로 이 문제를 해결하였다. 반환자료형이 없을 때는 Return을 Unit 형태로 선언하면 된다. 고차함수와 람다는 생소해서 잘 쓰지 않게되는 문법이지만, 필요할때가 종종 있어 인자에 함수를 넣는 형태로는 사용할때가 있다.. 모양이 어색해서 '이게 잘 쓰는건가?' 싶을때가 많지만 어쨌든 코틀린의 편리한 특징임은 틀림 없는듯 하다

 

쇼핑몰 프로젝트를 진행하면서, 뒤로가기 시 스크롤 유지 관련한 이슈 해결 히스토리를 기록해보고자 이 글을 작성하게 되었다. 현재 프로젝트에서는 Vue + Vue Router + Vuex 를 사용한다.

 

 

개요

현재 진행하던 프로젝트에서는 기존에 sessionStorage에 스크롤 위치를 저장하고, updated 훅에서 모든 화면 update가 끝난 후 sessionStorage에 있는 scroll값을 가져와 scroll을 이동시키는 스크롤을 컨트롤 하고 있었다. 이과정에서 서버 통신 후 데이터를 렌더링하는 시점과, 스크롤을 이동시키는 시점에 오류가 생겨 스크롤을 잘 잡지 못하는경우가 발생하여 로직을 수정하게 되었다.

 

고려해본 방법들 

1. keep-alive 적용

2. web storage 사용

3. router savedPosition 사용

 

Keep-alive 적용

keep-alive는 컴포넌트를 전환할 때, 성능상의 이유로 상태를 유지하거나, 재 렌더링을 피할 수 있게 해주는 element입니다. 재 렌더링을 피할 수 있기 때문에, 뒤로가기 시 별다른 장치 없이 그대로 화면을 유지할 수 있는 장점이 있다. 

<keep-alive>
<component v-bind:is="currentTabComponent"></component>
</keep-alive>

 

사용은 이런식으로 원하는 컴포넌트를 <keep-alive> 태그로 감싸주면 된다.

여기서 유의할점은 화면 전체의 재 렌더링을 피하고 싶다면 router-view 전체를 keep-alive 태그로 감싸야 한다는 것이다. (이것때문에 여러번 실패를 했었네요..) 

뒤로가기를 누를때만 적용 되야 하기 때문에 동적으로 할당 해야한다. 또한 재렌더링이 안되기 때문에 신중히 사용해야한다.

 

Web Storage 사용

화면 데이터와 스크롤 위치를 localStorage나 sessionStorage에 담아놓고, 돌아올 때 그대로 다시 불러오는 방법. 새롭게 서버통신을 해서 다시 리스트를 불러오지 않기 때문에 infinity scroll 사용 시 스크롤 위치를 잡기가 용이하다. 또 라이프사이클 훅에서 이루어지는 모든 것들이 정상 동작하므로 keep-alive를 사용할 수 없는 경우(라이프사이클을 거쳐야 하는 경우) 사용하기 좋다.

 

router savedPosition 사용

vue router는 scroll을 컨트롤 하기 위한 scrollBehavior 라는 함수를 제공하고 있다. 이는 HTML 히스토리 모드에서만 작동하는데, 라우터 객체를 만들때

const router = new VueRouter({
  routes: [...],
  scrollBehavior (to, from, savedPosition) {
    // 원하는 위치로 돌아가기
  }
})

이 안에 함수를 넣어서 사용하면 된다.

 

함수 안에서 리턴 값에 따라 스크롤의 위치가 변경되는데, 세번째 전달인자 (여기서는 savedPosition)은 브라우저가 앞/뒤 버튼으로 트리거 될때만 사용 가능하다

scrollBehavior (to, from, savedPosition) {
  if (savedPosition) {
    return savedPosition
  } else {
    return { x: 0, y: 0 }
  }
}

이런식으로 사용하면 되는데, 무조건 맨 위로 올리고 싶을 때는 

scrollBehavior (to, from, savedPosition) {
  return { x: 0, y: 0 }
}

이렇게 항상 0을 리턴하면 된다.

 

또 부드럽게 스크롤을 하도록 동작을 컨트롤 할 수도 있는데,

scrollBehavior (to, from, savedPosition) {
  if (to.hash) {
    return {
      selector: to.hash,
      behavior: 'smooth',
    }
  }
}

 이렇게 behavior에 스크롤 옵션을 입력 하면 된다. (selector는 위에서 설명했던 스크롤 위치이다)

 

 

이 이슈를 어떻게 해결하게 되었는지 전체적인 플로우는 다음장에서 설명하겠다.

다음은 카카오 로그인 릴리즈 해시키를 등록해보겠습니다.

 

일단 윈도우에서 릴리즈 해시키를 쉽게 얻으려면 크롬에서 제공해주는 리눅스 터미널 TermLinux를 사용해야합니다.

https://chrome.google.com/webstore/detail/termlinux-terminal-for-co/hlgjjjociconbkooaggfmjhalogadcee

 

TermLinux 명령 행을위한 터미널

소형 온라인 리눅스 가상 머신에 명령 행 콘솔을 제공하는 웹 터미널

chrome.google.com

1편에서 얻었던 SHA-1키를 복사한후 TermLinux 터미널에 

echo <SHA-1 키 입력> | xxd -r -p | openssl base64

명령어를 입력합니다.

 

명령어 입력후 나온 해시키를 복사하여

카카오 개발자 홈페이지 앱설정 -> 플랫폼 -> 키 해시에 복사한 해시키를 붙여 넣습니다.

 

플랫폼 선택
키 해시 추가

이제 완료되었으니 릴리즈로 테스트 해봅니다!

+ Recent posts