firebase with coroutines

문제 상황

firebase를 이용하다가 보면 라이브러리에서 제공하는 대부분의 메서드는 listener를 통해 비동기로 응답을 보내주고, 그 응답을 가공하여 사용해야 하는 경우가 많다

예를 들어

fun subscribeWalkingDistanceData(successCallback : () -> Unit, failureCallback : () -> Unit){
    Fitness.getRecordingClient(GlobalApplication.getApplicationContext(),mGoogleSignInAccount).subscribe(DataType.TYPE_DISTANCE_DELTA)
.addOnSuccessListener{
	Log.d(TAG,"successfully subscribe distance delta")
	successCallback()
}.addOnFailureListener{
		Log.e(TAG,"there was a problem subscribing walking distance data${it.localizedMessage}")
	failureCallback()
	}
}

위와 같이 유저가 이동한 거리에 대한 데이터를 subscribe를 하기 위해 다음과 같은 메서드를 작성했다면, 각각 성공 실패 리스너를 통해 firebase가 어떤 응답을 주는가에 따라서 매개변수로 받은 콜백함수를 실행시키는 형태로 코드를 작성 할 수 있다.

그런데 이렇게 코드를 작성하다 보면 콜백지옥의 문제가 생긴다.

depth가 하나일 때는 무난하게 callback함수를 매개변수로 넘겨 실행 시켜도 문제가 없지만

depth가 깊어질 수록 매개변수로 콜백을 계속 물고 들어가기 때문에 점점 복잡해지는 문제가 생긴다.

사실 이전에는 콜백으로 처리하였는데, 이번에 MVP패턴을 적용하면서 함수를 단순한 기능의 형태로 쪼개다보니 파이어베이스에서 받아온 응답 데이터를 그대로 return 해줘야 하는 함수들을 만들기 시작했고, 이에 다른 방법을 찾아보게 되었다.

해결 방법

구글링 결과 이를 해결하기 위해서 파이어베이스 메서드를 비동기처리하는 라이브러리를 추가하면 된다는 사실을 알게되었다.

https://github.com/Kotlin/kotlinx.coroutines/tree/master/integration/kotlinx-coroutines-play-services

  1. 모듈 수준 gradle에 dependency를 추가해주자

implementation **'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.6.2'**

  • 현재기준 1.6.2가 가장 최신 버전이다.
  1. dependency를 추가한 후 firebase에서 제공하는 라이브러리 안의 함수를 호출할 때 .await()를 사용할 수 있게 되어 응답이 올 때 까지 대기 할 수 있게 된다.
  2. 코드를 바꿔보자.
/*하루동안 걸은 거리 관련 함수 */
//피트니스 데이터(걸은 거리)
suspend fun subscribeWalkingDistanceData(){
    Fitness.getRecordingClient(GlobalApplication.getApplicationContext(),mGoogleSignInAccount).subscribe(DataType.TYPE_DISTANCE_DELTA)
     .addOnSuccessListener{
		}.addOnFailureListener{
	}

val response = Fitness.getRecordingClient(GlobalApplication.getApplicationContext(),mGoogleSignInAccount)
        .subscribe(DataType.TYPE_DISTANCE_DELTA).await()
}

위처럼 리스너에 콜백 함수를 지정하는 방식에서 밑에 await를 통해 응답이 올때까지 대기 한 후 response라는 변수에 응답으로 온 데이터를 할당하는 방식으로 바꿀 수 있다.

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객체를 생성하여 설정한 간격으로 좌표값을 가져온다.

 

환경 변수 란 ?

어떠한 프로세스가 실행될 때 영향을 미치는 동적인 값

OS 단에 선언되어있는 배포 환경에 따라 값을 동적으로 변경하기 위한 변수

내가 하고 싶었던 건?

환경변수인 NODE_ENV 값을 local, development, production으로 각각 나누어 의도한 바에 따라 .env파일을 분기하여 사용하고 싶었다

시행착오

  1. process.env 환경변수를 vue.파일의 script 태그 안에서 콘솔로 로그를 찍으니 계속 undefined를 얻었다.. → client side에서는 process.env를 사용 할 수 없다는 사실을 모르고 있었다. 알고보니 process.env는 server side에서만 사용이 가능했고, clientside에서도 활용이 가능하게 하기 위해 nuxt에서는 nuxt.config.ts 파일 안에 config 설정을 따로 두고 있었다.
  2. cmd 에서 환경변수를 set하는 방법을 몰라서 한참을 헤맸다. package.json 의 scripts 부분 안에 있는 명령어들을 지정할 때이런 방식으로 환경변수를 셋팅 하니 잘 동작했다.
  3. SET 환경변수명=값 & nuxt build
  4. SET NODE_ENV를 변경해도 저절로 .env 파일을 찾아가지 못했다. 예전 Vue CLI를 이용해서 프로젝트를 생성했을때는 뒤에 --mode local 이런 방식으로 지정하면 .env.local 파일의 변수값을 바라보게 자동으로 셋팅이 되었는데, Nuxt3는 그렇게 되지 않았고const phase = process.env.PHASE!!주의할 점!!따라서 nuxt.config.ts 안에서 process.env값을 runtimeConfig로 정의하여 컴포넌트 안에서 사용해야한다.
  5. nuxt에서는 process.env 값을 component안에서 console로 찍어 확인하면 nuxt.config.ts 안에서의 process.env 값과 다르다.
  6. require(’dotenv’).config({path:./.env${phase}})
  7. 해결방법으로 스크립트에 PHASE라는 환경변수값을 셋팅하고, 그값을 찾아와 nuxt.config.ts 파일 에서 dotenv의 파일 path값을 동적으로 바꿔주었다. ex) SET PHASE=local&&nuxi dev

CSR (Client-side only rendering)

과정

  1. 브라우저가 빈 HTML Document를 다운받는다
  2. 브라우저가 JS를 다운받고 실행시킨다
  3. 앱이 렌더링 되고 interactive 해진다

장점

  1. 서버와의 호환을 생각할 필요가 없기 때문에 개발 속도가 빠르다.
  2. SSR은 자바스크립트를 지원하는 플랫폼에서 서버를 가동하는데 비용이 들고, CSR은 정적인 HTML, CSS, JS파일들만 호스팅 하는 서버만 필요하기 때문에 상대적으로 비용이 저렴하다.
  3. 전체적으로 코드가 브라우저에서 실행되기 때문에 인터넷이 가능하지 않은 환경에서도 계속 실행이 가능하다.

단점

  1. 브라우저가 다운로드, 파싱, 자바스크립트를 실행 시킬 때까지 기다려야 하기 때문에, 다운로드하는 네트워크와 사용자의 디바이스에 따라 시간이 오래 걸릴 수 있음
  2. search engine이 첫시도에 페이지가 완전히 렌더링 될 때까지 기다릴 수 없기 때문에 SEO가 어렵다.

Universal Rendering

브라우저가 URL을 요청하면 서버는 완전히 렌더링 된 HTML페이지를 브라우저에게 보낸다. 페이지가 미리 생성되었던 캐싱되었던 Nuxt는 서버 환경에서 자바스크립트를 실행 시키고 HTML을 만들어낸다. CSR과 다르게 사용자는 애플리케이션의 내용을 즉각적으로 볼 수 있다. 이 단계는 정통적인 server-side rendering 방식과 비슷하다

CSR의 장점도 함께 가져가기 위해 HTML document가 다운로드 되었을 때 Client는 자바스크립트 코드를 백그라운드에서 실행 시킨다. 브라우저는 코드를 다시 해석하고 vue.js는 애플리케이션을 interactive하게 만든다.

정적 페이지를 브라우저에서 interactive하게 만드는 것을 Hydration이라고 한다.

Universal rendering은 nuxt 애플리케이션이 빠르게 페이지를 다운로드하는 동시에 CSR의 장점도 함께 보존한다. 게다가 애플리케이션의 content가 HTML document에 이미 존재한 채로 브라우저에 전달 되기 때문에 크롤러가 overhead없이 그것을 스캔할 수 있다.

과정

  1. 완전한 HTML이 브라우저로 전달되고 렌더링 된다
  2. 브라우저가 백그라운드에서 자바스크립트를 다운로드하고 실행 시킨다
  3. Hydration step이 완전히 끝나고 애플리케이션이 완전히 interactive해진다

장점

  1. 사용자가 바로 페이지에 접근이 가능하기 때문에 브라우저가 정적 컨텐츠를 더 빨리 보여줄 수 있다.
  2. Universal rendering은 완전한 HTML을 전달 하기 때문에 웹 크롤러가 페이지의 내용을 바로 찾을 수 있기 때문에 SEO가 가능하다.

단점

  1. 서버와 브라우저 환경이 같은 API를 제공하지 않거나, 서버와 브라우저 간 이질감 없이 실행되기 위해 코드를 짜는것이 까다로울 수 있다.
  2. 화면 렌더링을 위해 서버는 가동 되어야 한다. 이는 전통적인 서버와 같이 비용이 들어간다. 그러나 client side rendering navigation을 통해 비용이 많이 감소된다.

features

Assets

  • public : server root에서 파일 그대로 브라우저로 전달함.
  • assets : build tool에 의해 처리가 됨

Vite나 Webpack의 주기능은 자바스크리트 파일들을 처리하는 것임. 그러나 plugins(Vite)나 loaders(Webpack) 을 통해 다른 형태의 assets들도 처리 함( ex) stylesheets, fonts, SVG 등)

이 작업은 원래의 파일을 주로 성능이나 캐싱을 위한 절차 임( style파일을 최소화, 브라우저 캐시무효화)

Head Management

useHead Composable

ex)

<script setup>
useHead({
  titleTemplate: 'My App - %s',
  // or, instead:
  // titleTemplate: (title) => `My App - ${title}`,
  viewport: 'width=device-width, initial-scale=1, maximum-scale=1',
  charset: 'utf-8',
  meta: [
    { name: 'description', content: 'My amazing site.' }
  ],
  bodyAttrs: {
    class: 'test'
  }
})
</script>

setup function 안에서 useHead메서드를 사용해서 meta속성들(title, titleTemplate, base, script, style, meta, link, htmlAttrs, bodyAttrs)을 커스터마이징 해 사용할 수 있음. charset, viewport속성은 예시와 같이 chartset, viewport key값에 value를 설정해 쉽게 커스텀 할 수 있음

Meta Components

Nuxt는 metadata를 vue component template에서 사용할 수 있게 <Title>, <Base>, <Script>, <Style>, <Meta>, <Link>, <Body>, <Html>and <Head> 컴포넌트를 제공함

<script setup>
const title = ref('Hello World')
</script>
<template>
	<div>
		<Head>
			<Title>{{ title }}</Title>
			<Meta name="description" :content="title" />
			<Style type="text/css" children="body { background-color: green; }" />
		</Head>
		<h1>{{ title }}</h1>
	</div>
</template>

usage with definePageMeta

현재의 route의 metadata를 설정하기 위해 useHead 메서드와 definePageMeta메서드는 함께 사용가능함

<script setup>
definePageMeta({
  title: 'Some Page'
})
</script>

위의 예시는 page에 해당하는 vue파일임

<script setup>
const route = useRoute()

useHead({
  meta: [{ name: 'og:title', content: `App Name - ${route.meta.title}` }]
})
</script>

위의 예시는 layout파일에 해당함.

각각 page에 해당하는 파일에 meta 속성을 설정 해놓고, layout 파일에서 route의 현재 route의 메타데이터를이용해 html 데이터를 설정 할 수 있음.

Data Fetching

Nuxt는 데이터를 핸들링 하기 위해 useFetch, useLazyFetch, useAsyncData, useLazyAsyncData, 를 제공함 ⇒ 이 메서드들은 setup이나, lifecycle hook에서만 사용 가능함.****

useAsyncData

page, component, plugin에서 비동기적으로 데이터를 받을 때 useAsyncData를 사용할 수 있음

ex)

let counter = 0
export default () => {
  counter++
  return JSON.stringify(counter)
}

위의 예시는 server/api/count.ts 파일

<script setup>
const { data } = await useAsyncData('count', () => $fetch('/api/count'))
</script><template>
  Page visits: {{ data }}
</template>

count의 data를 받아 올때는 위와 같이 사용함

useLazyAsyncData

useAsyncData에 lazy:true 옵션을 추가한 것과 동일함.

async function 이 페이지 전환을 막지않음. data가 null일때도 data를 핸들링 할 수 있음

<template>
	<div>
    {{ pending ? 'Loading' : count }}
  </div>
</template>
<script setup>
const { pending, data: count } = useLazyAsyncData('count', () => $fetch('/api/count'))
watch(count, (newCount) => {
  // Because count starts out null, you won't have access
  // to its contents immediately, but you can watch it.
})
</script>

useFetch

page, component, plugin 파일 안에서 useFetch 메서드를 사용 할 수 있음

useFetch는 어느 URL이든 접근하여 데이터를 fetch 할 수 있게 하는 메서드임.

이 메서드는 useAsyncData와 $fetch를 함께 포함하고 있는 개념이기 때문에 사용하기 편리함.

<script setup>
const { data } = await useFetch('/api/count')
</script><template>
  Page visits: {{ data.count }}
</template>

useLazyFetch

useFetch에 lazy:true 옵션을 추가한 것과 동일함.

async function이 페이지 전환을 막지 않음

data가 null일때도 핸들링이 가능함.

<template><!-- you'll need to handle a loading state -->
  <div v-if="pending">
    Loading ...
  </div>
	<div v-else>
		<div v-for="post in posts">
			<!-- do something -->
    </div>
	</div>
</template>
<script setup>
const { pending, data: posts } = useLazyFetch('/api/posts')
watch(posts, (newPosts) => {
  // Because posts starts out null, you won't have access
  // to its contents immediately, but you can watch it.
})
</script>

Refreshing Data

data 요청을 다시 보내 data를 새로 load해야 하는 겨우. refresh API를 사용할 수 있음.

useAsyncData 를 이용해 refresh 메서드를 받아 사용함

<script setup>
const page = ref(1);

const { data:users, loading, refresh, error } = await useFetch(() => `users?page=${page.value}&take=6`, { baseURL: config.API_BASE_URL }
);

function previous(){
  page.value--;
  refresh();
}

function next() {
  page.value++;
  refresh();
}
</script>

refreshNuxtData

useAsyncData, useLazyAsyncData, useFetch, useLazyFetch 의 캐시를 무효화 하고, refetch하기 위한 메서드.

현재 페이지의 모든 data를 새롭게 refresh 할때 사용

<template><div>
    {{ pending ? 'Loading' : count }}
  </div><button @click="refresh">Refresh</button></template><script setup>
const { pending, data: count } = useLazyAsyncData('count', () => $fetch('/api/count'))

const refresh = () => refreshNuxtData('count')
</script>

Isomorphic fetch and $fetch

브라우저에서 fetch를 사용할때, user의 헤더(ex)cookie)는 바로 API로 보내진다. 그러나 server-side-rendering에서 fetch 요청은 서버 안에서 발생하게 되기 때문에 이는 브라우저의 cookie를 전송 받지 못하고, 응답에도 cookie를 보낼 수 없다.

Pass client headers to the API

이 문제는 useRequestHeaders를 사용해서 server-side의 API에서 cookie를 접근할 수 있음

<script setup>
const { data } = useFetch('/api/me', {
  headers: useRequestHeaders(['cookie'])
})
</script>

위는 브라우저에서 cookie를 받아 사용하는 예시

Pass on cookies from server-side API calls on SSR response

만약 cookie를 반대로 응답에 포함하여 전달하고 싶을 때는 아래 예시와 같이 사용

export const fetchWithCookie = async (url: string, cookieName: string) => {
  const response = await $fetch.raw(url)
  if (process.server) {
    const cookies = Object.fromEntries(
      response.headers.get('set-cookie')?.split(',').map((a) => a.split('='))
    )
    if (cookieName in cookies) {
      useCookie(cookieName).value = cookies[cookieName]
    }
  }
  return response._data
}
<script setup lang="ts">
// This composable will automatically pass on a cookie of our choice.
const result = await fetchWithCookie("/api/with-cookie", "test")
onMounted(() => console.log(document.cookie))
</script>

Using async setup

async setup() composable에는 처음 await를 사용하고 나서 현재 component의 instance가 손실됨.

따라서 useFetch같은 메서드를 여러번 호출 하기 위해서는 <script setup>을 사용하거나 함께 묶어서 await를 걸어야 함

State Management

useState

SSR-frendly한 반응형을 만들기 위해 Nuxt는 useState라는 composable을 제공함.

이 메서드를 이용해 만든 value는 server-side 렌더링 이후에도 보존됨.

모든 컴포넌트들에서 공유됨.

Basic usage

<script setup>
const counter = useState('counter', () => Math.round(Math.random() * 1000))
</script>
<template>
	<div>
    Counter: {{ counter }}
    <button @click="counter++">
      +
    </button><button @click="counter--">
      -
    </button>
	</div>
</template>

위 예시는 component-local counter state를 사용하고 있음. 하지만 다른 컴포넌트들 역시 useState(’counter’)로 같은 반응형 state를 공유한다.

Error Handling

Nuxt3는 full-stack framework로 예측불허한 다양한 런타임 에러들이 다양한 context에서 발생할 수 있음.

  • Errors during the Vue rendering lifecycle(SSR + SPA)
  • Errors during API or Nitro server lifecycle
  • Server and client startup errors(SSR + SPA)

Errors during the Vue rendering lifecycle (SSR + SPA)

Vue 에러를 onErrorCaptured 에서 핸들링 할수 있음

또한 Nuxt는 에러가 발생하면 상위 레벨로 에러를 전파 시켜 vue:error 훅이 호출 됨

vueApp.config.errorHandler에서 핸들링 가능한 모든 Vue에러를 받을 수 있음

Server and client startup errors (SSR + SPA)

Nuxt는 Nuxt 애플리케이션이 시작할 때 에러가 발생하면 app:error 훅을 호출한다.

이는

  • plugin을 실행할 때
  • app:created, app:beforeMount 훅을 처리할 때
  • app이 mount될때의 에러는 onErrorCaptured 나 vue:error 훅에서 처리해야함
  • app:mounted 훅을 처리할 때

호출된다.

Errors during API or Nitro server lifecycle

server-side에서 handler를 지정 할 수는 없지만, 에러 페이지를 띄울 수 있음.

Rendering an error page

Nuxt가 렌더링 시 client side나 server side lifecycle에서 에러를 만나면 JSON 응답 또는 HTML 에러 페이지를 렌더링 함(요청 헤더에 Accept:application/json 이 있다면)

이 에러 페이지는 app.vue와 같은 레벨의 source directory의 ~/error.vue로 만들어서 커스터마이징 할 수 있다. 만약 error 페이지를 지우고 싶다면 clearError helper 함수로 다른 페이지로 에러 발생시 리다이렉트 시킬 수 있다.

<template>
	<button @click="handleError">Clear errors</button>
</template>
<script setup>
const props = defineProps({
  error: Object
})

const handleError = () => clearError({ redirect: '/' })
</script>

Error helper methods

  • useError

→ 전역으로 발생하는 Nuxt 에러를 핸들링 할 수 있음

  • throwError

→ 이 함수는 client side, server side에서 middleware, plugin, setup함수에서 언제든지 사용 가능함. 이 함수는 트리거가 될때마다 에러페이지를 반환함. 이 에러페이지는 clearError 함수로 clear시킬 수 있음

  • clearError

→ 이 함수는 핸들링 된 Nuxt error를 clear하며 다른 페이지로 redirect 할 수 있음

Rendering errors within your app

<NuxtErrorBoundary> 컴포넌트는 다른 에러 페이지를 생성하지 않고도 에러페이지를 렌더링 하게 해줌.

이 컴포넌트는 반응형으로 에러를 핸들링 할 수 있음.

<template><!-- some content -->
  <NuxtErrorBoundary @error="someErrorLogger">
<!-- You use the default slot to render your content -->
    <template #error="{ error }">
      You can display the error locally here.
      <button @click="error = null">
        This will clear the error.
      </button>
		</template>
	</NuxtErrorBoundary>
</template>

Server Routes

Nuxt는 자동으로 /server/api, /server/routes, /server/middleware 디렉토리에 들어있는 파일들을 스캔하고 API와 서버 핸들러에 등록함

이 핸들러는 JSON 데이터를 리턴하며, Promise, event.res.end() 를 사용하여 응답을 보낸다.

ex) server/api/hello.ts 파일을

export default defineEventHandler((event) => {
  return {
    api: 'works'
  }
})

전역적으로 await $fetch(’/api/hello’)로 API를 호출 할 수 있음

Server Routes

~/server/api 안의 파일들은 prefix로 /api라는 path를 자동으로 갖게 되며, /api라는 prefix가 붙지 않고 사용하려면 ~/server/routes 디렉토리에 추가 해야함.

예를들어 ~/server/routes/hello.ts 라는 파일을 아래와 같이 작성하면

export default defineEventHandler(() => 'Hello World!')

http://localhost:3000/hello 로 접근 할 수 있음

Server Middleware

Nuxt는 ~/server/middleware 디렉토리 안에 있는 모든 파일들을 자동으로 읽고 server middleware를 생성함.

middleware handler는 모든 요청에 server의 route를 거치기 전 헤더를 체크하거나 로직을 추가 하는데 사용 함.

ex) /server/middleware/log.ts

export default defineEventHandler((event) => {
  console.log('New request: ' + event.req.url)
})

ex) /server/middleware/auth.ts

export default defineEventHandler((event) => {
  event.context.auth = { user: 123 }
})

Matching Route Parameters

server routes는 동적으로 파라미터를 가져올 수 있다.

예를들어 /api/hello/[name].ts 파일의 이름을 event.context.params.name으로 찾아올 수 있음

ex) /server/api/hello/[name].ts

export default defineEventHandler(event => Hello, ${event.context.params.name}!)

await $fetch(’/api/hello/nuxt’)를 호출하면 Hello, nuxt!를 얻을 수 있음

Matching HTTP Method

파일 이름 뒤에 HTTP Method(.get, .post, .put, .delete ...)를 붙여서 핸들링 할 수 있음.

ex) /server/api/test.get.ts

export default defineEventHandler(() => 'Test get handler')

ex) /server/api/test.post.ts

export default defineEventHandler(() => 'Test post handler')

위의 예시들은 각각 get 핸들러 , post 핸들러를 리턴함

Catch-all route

Catch-all route는 루트 path 이후에 catch-all route가 정해져 있지 않으면 해당 path를 포함하는 모든 요청에 대한 핸들러를 작성 할 수 있음

ex) /server/api/foo/[...].ts

export default defineEventHandler(() => Default foo handler)

ex) /server/api/[...].ts

export default defineEventHandler(() => Default api handler)

Handling Requests with Body

ex) /server/api/submit.post.ts

export default defineEventHandler(async (event) => {
    const body = await useBody(event)
    return { body }
})

예시에서 $fetch(’/api/submit’, {method: ‘post’, body :{test:123}})로 호출 할 수 있음

.post.ts 파일은 useBody를 사용할 수 있음, 만약 .get.ts 파일을 핸들링 하려고 useBody를 쓰면 405 에러가 남

Handling Requests with query parameters

ex) /server/api/query.get.ts

export default defineEventHandler((event) => {
  const query = useQuery(event)
  return { a: query.param1, b: query.param2 }
})

Get access to the runtimeConfig

ex) /server/api/foo.ts

export default defineEventHandler((event) => {
  const config = useRuntimeConfig()
  return { key: config.KEY }
})

Access Request Cookies

ex)

export default defineEventHandler((event) => {
  const cookies = useCookies(event)
  return { cookies }
})

Runtime Config

Nuxt는 런타임에 config를 컨트롤 할 수 있는 config API를 제공함.

Exposing runtime config

nuxt.config파일의 runtimeConfig 옵션에 런타임 설정을 할 수 있음

ex) nuxt.config.ts

export default defineNuxtConfig({
  runtimeConfig: {
    // The private keys which are only available within server-side
    apiSecret: '123',
    // Keys within public, will be also exposed to the client-side
    public: {
      apiBase: '/api'
    }
  }
})

Environment Variables

Nuxt CLI 는 built-in 으로 dotenv를 지원함

만약 .env 파일이 프로젝트 root 디렉토리에 존재하면 process.env에 자동으로 load 되고 module과 nuxt.config에서 접근 가능함

ex) .env

NUXT_API_SECRET=api_secret_token
NUXT_PUBLIC_API_BASE=https://nuxtjs.org

ex) nuxt.config.ts

export default defineNuxtConfig({
  runtimeConfig: {
    apiSecret: '',
    public: {
      apiBase: '', // Or a default value
    }
  },
})

Accessing runtime config

  • Vue app

런타임 config에 접근 하기 위해 useRuntimeConfig() 를 호출함

ex)

<template><div><div>Check developer console!</div></div></template><script setup>
const config = useRuntimeConfig()
console.log('Runtime config:', config)
if (process.server) {
  console.log('API secret:', config.apiSecret)
}
</script>

client side에서는 config.pubic만 접근 가능하며 읽기/쓰기가 가능함

server side에서는 모든 key에 접근 가능하지만, 쓰기가 불가능함

useRentimeConfig()는 setup이나 Lifecycle hook에서만 사용가능함

  • Plugins

plugin에서 useRuntimeConfig를 사용하려면 defineNuxtPlugin 안에서 사용함

ex)

export default defineNuxtPlugin((nuxtApp) => {
  const config = useRuntimeConfig()
  console.log('API base URL:', config.public.apiBase)
});
  • Server Routes

ex)

export default async () => {
  const result = await $fetch('<https://my.api.com/test>', {
    headers: {
      Authorization: `Bearer ${useRuntimeConfig().apiSecret}`}
  })
  return result
}

Teleports

Vue3는 <Teleport> 컴포넌트를 제공함. 이는 Vue application 이 아닌 DOM의 어느곳에서든지 렌더링이 됨

Nuxt는 SSR에서 teleport는 body만 가능함, client-side에서 사용은 <ClientOnly> warpper로 감싸야 함

  • body teleport

ex)

<template>
	<button @click="open = true">
    Open Modal
  </button>
	<Teleport to="body">
		<div v-if="open" class="modal">
			<p>Hello from the modal!</p>
			<button @click="open = false">
        Close
      </button>
		</div>
	</Teleport>
</template>
  • client-side teleport

ex)

	<ClientOnly>
		<Teleport to="#some-selector">
				<!-- content -->
	  </Teleport>
	</ClientOnly>
</template>

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 파일에선 위와 같이 사용함

1. *

ex)

* {

**margin**: 0;

**padding**: 0;

}

페이지에 있는 전체 요소를 대상으로 함. *를 자주 쓰면 브라우저에 과부하가 걸림. 사용하기에 적절하지 않음.

2. #X

ex)

#container {

**width**: 960px;

**margin**: auto;

}

선택자 앞에 해시(#) 기호를 붙여서 id를 대상으로 삼음. id는 재활용 할 수 없기 때문에 조심스러워야 함

3. .X

ex)

.error {

**color**: red;

}

class 선택자. 여러개의 요소를 대상으로 정할 수 있음. 한 그룹의 요소에 스타일을 적용 할 때 class를 사용함.

4. X Y

ex)

li a {

**text-decoration**: none;

}

descendant 두개의 선택자를 조합하여, 뒤쪽 선택자 요소의 조상에 앞쪽 선택자 요소가 존재할 경우 선택. 자손 결합자를 활용하는 선택자를 자손 선택자라고 부름.

5. X

ex)

a { **color**: red; }

ul { **margin-left**: 0; }

type 선택자. type에 따라 모든 요소를 대상으로 삼고 싶을 때 사용

6. X:visited 와 X:link

ex)

a:link { **color**: red; }

a:visted { **color**: purple; }

클릭하기 전 상태의 앵커 태그 전체를 대상으로 하려고 :link 가상 클래스를 사용

:visited 가상클래스로 하기도 함. 이는 클릭했었거나 방문했던 페이지에 있는 태그에 특정한 스타일을 적용 할 수 있음.

7. X + Y

ex)

ul + p {

**color**: red;

}

인접 선택자로 부르는 선택자. 앞의 요소 바로 뒤에 있는 요소만 선택함. 위 예시에서는 ul뒤에 오는 첫번째 단락의 텍스트만 선택됨.

8. X > Y

ex)

div#container > ul {

**border**: 1px solid black;

}

일반 X Y와 X > Y의 차이점은 후자가 ‘직계’ 자식만을 선택한다는 것. 자손의 자식들은 선택되지 않음

9. X ~ Y

ex)

ul ~ p {

**color**: red;

}

이 형제 선택자는 X + Y와 유사하지만 덜 엄격 함. 인접 선택자( + )는 앞의 선택자 바로 뒤에 오는 첫번째 요소만 선택하지만, ~는 예시에서 ul 아래있는 모든 p요소를 선택함.

10. X[title]

ex)

a[title] {

**color**: green;

}

속성 선택자 라고 말하며 예시에서 title 속성이 있는 앵커 태그만을 선택함. title이 없는 앵커 태그에는 특정한 스타일이 적용되지 않음.

11. X[href=”foo”]

ex)

a[href="**[<https://net.tutsplus.com>](<https://net.tutsplus.com/>)**"] {

**color**: #1f6053; /* nettuts green */

}

예시는 https://net.tutsplus.com 로 연결된 전체 앵커 태그에 스타일이 적용됨.

12. X[href="nettuts"]*

ex)

a[href*="tuts"] {

**color**: #1f6053; /* nettuts green */

}

별표는 입력값이 속성값 안 어딘가에 보여야 한다는 것을 표시함. 예시처럼 작성되었을 경우

nettuts.com, net.tutsplus.com tutsplus.com 등 tuts가 포함되는 url모두를 포함함

13. X[href^="http"]

ex)

a[href^="http"] {

**background**: url(path/to/external/icon.png) no-repeat;

**padding-left**: 10px;

}

캐럿기호 ^는 문자열의 시작을 표기하는 정규 표현식에서 흔히 사용됩니다.

14. X[href$= “.jpg”]

ex)

a[href$=".jpg"] {

**color**: red;

}

예시는 문자열 끝에 적용하도록 정규 표현식 기호인 $를 사용.

15. X****[data-="foo"]***

ex)

a[data-filetype="image"] {

**color**: red;

}

여러가지 이미지 형식을 어떻게 적용할때

ex)

<**a** href="path/to/image.jpg" data-filetype="image"> Image Link </**a**>

a[data-filetype="image"] {

**color**: red;

}

위 예시처럼 해당 앵커만 대상으로 삼는 속성 선택자를 사용할 수 있음

16. X[foo~="bar"]

ex)

a[data-info~="external"] {

**color**: red;

}

a[data-info~="image"] {

**border**: 1px solid black;

}

~를 이용하면 띄어쓰기로 구분되는 값이 있는 속성을 대상으로 할 수 있음.

ex)

"<**a**href="path/to/image.jpg" data-info="external image"> Click Me, Fool </**a**>

/* Target data-info attr that contains the value "external" */

a[data-info~="external"] {

**color**: red;

}

/* And which contain the value "image" */

a[data-info~="image"] {

**border**: 1px solid black;

}

17. X:checked

ex)

input[type=radio]:checked {

**border**: 1px solid black;

}

라디오 버튼이나 체크박스처럼 체크되는 사용자 인터페이스 요소만을 대상으로 함.

18. X:after

ex)

.clearfix:after {

**content**: "";

**display**: block;

**clear**: both;

**visibility**: hidden;

**font-size**: 0;

**height**: 0;

}

.clearfix {

***display**: inline-block;

**_height**: 1%;

}

선택된 요소 주변에 콘텐츠를 생성함. 요

19. X:hover

ex)

div:hover {

**background**: #e3e3e3;

}

사용자가 요소 위에 커서를 올릴 때 특정할 스타일을 적용 하고 싶을 때 사용

20. X:not(선택자)

ex)

div:not(#container) {

**color**: blue;

}

예시는 모든 div를 선택하고 싶은데, 그중에서 id가 container인 것만 제외하고 싶을 때

21. X::가상요소

ex)

p::first-line {

**font-weight**: bold;

**font-size**: 1.2em;

}

첫 번째 줄이나 첫 글자와 같이 요소 일부분에 스타일을 적용하는데 가상요소(::)를 사용할 수 있음. 효과를 보려면 이요소를 반드시 블록 레벨 요소에 적용해야 함

22. X:nth-child(n)

ex)

li:nth-child(3) {

**color**: red;

}

nth-child는 변수값을 정수로 받음. 1부터 시작. 두번째 항목을 대상으로 하려면 li:nth-child(2)로 작성4번째마다 선택하려면 li:nth-child(4n)으로 작성

23. X:nth-last-child(n)

ex)

li:nth-last-child(2) {

**color**: red;

}

끝에서 두 번째 항목만 선택할 때. 22번과 비슷하지만 집합의 끝에서 부터 출발하면서 동작하는 것이 다름

24. X:nth-of-type(n)

ex)

ul:nth-of-type(3) {

**border**: 1px solid black;

}

child를 선택하지 않고 요소의 type을 선택하고 싶을 때

예시는 세번 째 ul에만 스타일을 지정함

25. X:nth-last-of-type(n)

ex)

ul:nth-of-type(3) {

**border**: 1px solid black;

}

집합의 끝에서 출발해 지정한 요소를 대상으로함

26. X:first-child

ex)

ul li:first-child {

**border-top**: none;

}

부모 요소의 첫 번째 자식만 선택함. 목록에서 맨 처음과 맨 나중 항목에서 테두리 선을 제거하는데 이 방식을 흔히 사용함.

27. X:last-child

ex)

ul > li:last-child {

**color**: green;

}

부모 요소의 마지막 자식만 선택함

28. X:only-child

ex)

div p:only-child {

**color**: red;

}

부모의 단 하나의 자식 요소를 지정할 수 있음

ex)

<**div**><**p**> My paragraph here. </**p**></**div**>

<**div**>

<**p**> Two paragraphs total. </**p**>

<**p**> Two paragraphs total. </**p**>

</**div**>

두 번째 div 문단은 대상이 되지 않고 오직 첫번째 div가 대상이 됨. 하나 이상의 자식을 요소에 적용하는 순간 only-child 가상 클래스 효과는 사라지게 됨

29. X:only-of-type

ex)

li:only-of-type {

**font-weight**: bold;

}

형제 요소가 없는 요소를 대상으로 함 예로 단 하나의 목록 아이템인 ul 전부를 대상으로 삼음.

30. X:first-of-type

ex) 가상 클래스로 해당 type의 첫 번째 형제 선택자를 선택할 수 있음

자동 라우팅

pages 디렉토리 안에 있는 vue 파일들은 자동으로 라우팅 됩니다 ⇒ router.js 파일이 필요 없습니다.

동시에 code splitting 도 함께 제공합니다.

네비게이션

페이지간 이동에는 NuxtLink를 사용해야 합니다.

page 디렉토리는 view와 route를 담고 있습니다. Nuxt는 page디렉토리안의 모든 .vue파일을 읽고 그 파일에 해당하는 라우터를 만듭니다.

 

 

Nuxt SSR

Nuxt의 Server Side Rendering steps

1. Browser to Server

브라우저가 최초 요청을 보내면 내부 Node.js 서버에 도달합니다. Nuxt는 lifecycle hooks, asyncData, nuxtServerInit, fetch 등 실행된 함수의 결과들과 함께 HTML을 생성하고 이를 다시 브라우저로 돌려보냅니다.

2. Server to Browser

브라우저가 서버로부터 렌더링된 HTML 페이지를 받으면 vue.js hydration 을 적용하여 page를 상호작용할 수 있는 형태로 변경합니다.

3. Browser to Browser

브라우저 안에서 NuxtLink 태그를 이용하여 client side에서 네비게이션 합니다.

Nuxt SSG

Nuxt의 Static Site Generation steps

1. Browser to CDN

브라우저가 CDN으로 최초의 요청을 보냅니다.

2. CDN to Browser

CDN은 이미 만들어진 HTML 과, 자바스크립트, 정적 assets 파일을 브라우저로 보냅니다.

내용이 보여지고 Vue.js hydration이 일어나 상호작용이 가능한 형태로 변경됩니다.

3. Browser to Browser

클라이언트 사이드에서 네비게이팅이 일어나 더이상 CDN으로의 요청은 가지 않습니다. API Call은 이미 캐싱된 정적 폴더에서 로드됩니다.

+ Recent posts