kotlin lateinit, lazy by

개발/안드로이드 2021. 6. 5. 14:59 Posted by 아는 개발자

자바에서 흔히 보게 되는 NullPointerException 문제를 예방하고자 코틀린에서는 변수 선언에서부터 Nullable 변수의 선언부터 엄격하게 관리한다. 변수를 선언 할 때도 Nullable인지 아닌지를 구분해야하고 Nullable인 경우에는 변수를 호출하는 코드에서 Nullsafe 지시자를 표시해야하며 그렇지 않으면 컴파일 단계에서 에러를 발생시킨다.

 

var name: String? = null 
name = "abcd"
name?.length() // name이 여전히 null 일 가능성이 존재하므로, null safe 접근만 허용된다

var name2: String = "abcd"
name2 = null // name2가 nullable하지 않으므로 이 코드는 컴파일 오류가 발생한다

 

 

그런데 코드 상에서는 null이 될 수도 있지만 실제 동작 중에는 null이 될 소지가 없는 경우가 있다. 아래 코드처럼 전역 변수인데 실행과 동시에 초기화를 시키는 경우가 이렇다. 논리적으로는 안전한 코드임에도 불구하고 값에 접근 할 때 null safe 지시자를 표시해야하는 불편함이 생긴다. 

 

class TestActivity: Activity() {
    var name: String? = null
    override fun onCreate() {
        name = "abcd"
        print("${name?.length}")
    }
}

 

lateinit 

 

lateinit을 사용하면 변수의 값을 지정하는 작업을 뒤로 미룰 수 있다. Nullable 하지 않은 변수를 선언하면서 Assign 하는 작업을 뒤로 미루고 싶을때는 lateinit 키워드를 사용하면 가능하면 된다. 아래 코드는 name 변수 앞에 lateinit 키워드를 두고 onCreate 콜백에서 값을 바로 지정했다. 선언 당시 Non-Null String으로 선언했기 때문에 호출할 때 

 

class TestActivity: Activity() {
    lateinit var name: String
    override fun onCreate() {
        name = "abcd"
        print("${name.length}")
    }
}

 

lateinit은 mutable 변수만 가능하기 때문에 var 키워드를 가진 변수에서만 사용이 가능하다. 실행 중에 값을 변경할 필요가 있는 경우 유용하다. 그리고 만약 값을 assign하지 않고 변수 값을 호출하는 경우에는 Kotlin 언어 상에서 에러를 발생시킨다.

 

by lazy 

 

by lazy 키워드는 lateinit과 비슷하게 값을 지정하는 작업을 미루는 작업인데 assign 되는 시점이 변수를 호출하는 시점이다. 아래 코드를 보면 name 변수 선언에 by lazy 키워드가 붙고 내부 브래킷에 "abcd" 코드가 있다. name변수가 호출되는 시점에 "abcd"로 assign 하겠다는 의미다. 실제 코드를 동작시켜보면 name 호출 시점에 by lazy 내부 로그가 먼저 호출되는 것을 볼 수 있다.

 

class Test {
    val name : String by lazy {
        println("this is name by lazy")
        "abcd"
    }

    init {
        println("I am here")
        println(name)
    }
}

// 실행 결과
I am here
this is name by lazy
abcd

 

by lazy는 immutable 변수에서만 적용이 가능해, val 키워드 변수에만 적용이 가능하다. 변수 값을 최초에만 설정하고 변경할 필요가 없는 경우 사용하면 유용하다. 변수 선언시에 값을 assign할 순 없지만 다른 변수들을 조합해 값을 설정하고 싶을 때 사용하면 유용하다.

728x90

Kotlin - Coroutine

개발/안드로이드 2021. 5. 21. 20:00 Posted by 아는 개발자

Coroutine을 공부할 때 당장 실행되는 코드를 짜려고 launch, async 함수부터 먼저 써보게 되는데(과거의 나) 이것보단 Coroutine을 이루는 구조가 무엇인지를 먼저 공부하고 유틸리티 함수를 사용하면 훨씬 이해하기가 쉽다.  Coroutine을 이루는 구조는 크게 CoroutineScope과 CoroutineContext다. 아래 그림으로 보면 CoroutineScope이 CoroutineContext를 포함하는 관계다.

 

 

1. CoroutineScope

 

CoroutineScope은 Coroutine이 활동할 수 있는 범위를 말한다. 예를 들어 Coroutine이 ViewModel의 생성주기 내에서만 동작하게 할 수 있고 Activity Lifecyle 생명주기를 따라서 동작하게 할 수 있는데 CoroutineScope은 Coroutine의 활동범위를 말한다. 이 속성을 잘 이용하면 Component의 생성주기에 맞춰 Coroutine 작업을 자동으로 취소할 수 있어서 유용하다. Kotlin에서는 안드로이드에서 사용할 수 있도록 몇가지 CoroutineScope을 미리 만들어뒀다. 

 

  • GlobalScope: 앱 프로세스의 생명주기를 따라감. 
  • MainScope: UI 관련 작업을 처리하는 용도.
  • ViewmodelScope: ViewModel의 생성주기를 따라감.
  • LifecycleScope: Activity, Fragment의 생명주기를 따라감. 생명주기별로 콜백이 다르다.
MainScope().launch {}

GlobalScope.launch {}

 

CoroutineScope 인터페이스를 구현해서 커스텀한 CoroutineScope을 만들수도 있긴 한데 공식 문서에서 이 방법은 추천하진 않고 있다.

 

2. CoroutineContext 

 

CoroutineContext는 Coroutine을 이루는 정보다. Coroutine 이름, Job, Dispatcher, ExceptionHandler 가 이에 해당한다. Dispatcher는 Coroutine이 실행될 쓰레드 풀을 의미한다. 대표적으로 Main, IO 쓰레드 풀이 있어서 UI 작업의 경우에는 Main, 디스크 작업에는 IO 쓰레드를 사용하도록 지정 할 수 있다. Executors 라이브러리를 이용해 커스텀으로 만든 쓰레드 풀에도 지정이 가능하다. 아래 코드는 우선순위가 높은 쓰레드 풀에서 동작하는 CoroutineScope을 만든 예다.

 

val customExecutor: Executor = Executors.newCachedThreadPool { r ->
    Thread(r, "CustomThread").apply {
        priority = Thread.MIN_PRIORITY
    }
}
val customDispatcher = object : CoroutineDispatcher() {
    override fun dispatch(context: CoroutineContext, block: Runnable) {
        customExecutor.execute(block)
    }
}

CoroutineScope(customDispatcher).launch {

}

 

ExceptionHandler는 Coroutine 내의 코드 실행중 발생하는 Exception을 처리할 수 있는 Handler다. 현재 Scope 별로 Exception Handler를 다르게 둘 수 있기 때문에 이것도 잘 써먹으면 유용하다. 

 

val handler = CoroutineExceptionHandler { context, th->
    println("$context ${th.toString()} ")
}

GlobalScope.launch(handler) {
    val async1 = async(){ 1 }
}

 

3. Utility 함수 

 

3.1 launch

 

launch 함수는 CoroutineScope내에서 실행되며, 현재 쓰레드를 막지 않고(blocking) 동작할 수 있는 새로운 Coroutine Job을 생성한다. 병렬로 수행되기 때문에 여러가지 작업을 동시에 수행할 때 쓰면 좋다. 아래 코드는 GlobalScope 내에서 두개의 Coroutine Job을 생성한 코드다. 앞에 코드에 300ms 의 딜레이를 줬다. 그 결과 scope2가 먼저 프린트 되고, scope1은 그 이후에 프린트 된다.

 

GlobalScope.launch {
    launch {
        delay(300)
        println("scope1")
    }
    launch {
        println("scope2")
    }
}

2021-05-21 16:14:17.677 I/System.out: scope2
2021-05-21 16:14:17.978 I/System.out: scope1

 

Job 내부 함수인 join() 은 동시성을 제어할 수 있는 함수다. 현재 Coroutine의 실행이 종료되지 않을 때까지 다음 코드를 실행하지 않는다. 순서를 관리할 때는 이 함수를 쓰면 된다. 그리고 cancel() 처럼 취소할 수 있는 함수도 있다. 이건 실제로 사용하다 보면 어떻게 써야하는지 감이 온다.

 

3.2 async 

 

async 함수는 launch 와 거의 동일하고 결과 값을 받을 수 있다는 점이 추가 됐다. 아래 코드의 두 async Job은 각각 1, 2를 리턴하는 CoroutineScope이다. async 내부 await() 함수는 여기서 실행된 결과 값을 받아오게 된다. 여기서 주의깊게 볼 부분은 각각에 delay를 300ms, 100ms 씩 줬는데도 start 로그로부터 결과 값까지 걸린 시간은 둘의 합인 400ms가 아니라 가장 긴 delay인 300ms라는 점이다. 두 Job을 병렬로 처리했기 때문에 가장 delay를 오래 잡는 Job의 시간만큼 소요된다.

 

GlobalScope.launch {
    println("start")
    val a = async {
        delay(300)
        1
    }
    val b = async {
        delay(100)
        2
    }

    println("a + b = ${a.await() + b.await()}")
}

2021-05-21 16:22:19.885 I/System.out: start
2021-05-21 16:22:20.190 I/System.out: a + b = 3

 

3.3 withContext 

 

동일한 CoroutineScope 내에서도 종종 Coroutine Context를 바꿔야 할 일이 생긴다. 예를 들면 I/O 작업을 수행 중에도 중간중간 화면 UI를 업데이트 해야하는 경우 Dispatcher를 바꿔 코드에 적용하는 쓰레드를 변경해야한다. 이럴때 쓰면 유용한 함수가 withContext다. 아래 코드는 I/O 스레드 풀에서 "abc", "def"라는 문자열을 받아오고 두 문자를 합해서 text라는 변수를 만들었다. UI 에 적용하려면 withContext를 이용해 임시로 Main함수로 바꿔주어 UI 컴포넌트에 접근 할 수 있다. 참고로 withContext는 내부적으로 async{}.await()로 구현돼 있어 내부 코드가 모두 실행된 다음에 다음 코드로 넘어가게된다. 

 

CoroutineScope(Dispatchers.IO).launch {
    val a = async {
        "abc"
    }
    val b = async {
        "def"
    }
    
    val text = a.await() + b.await()
    
    withContext(Dispatchers.Main) {
        textView.text = text
    }
}
728x90

Android 10 스토리지 정책 대처하기

개발/안드로이드 2021. 5. 18. 20:40 Posted by 아는 개발자

targetSdkVersion 을 30으로 올리면 파일 절대 경로를 사용해서 접근 할 수 없기 때문에 개발자들은 지금부터 슬슬 절대 경로를 사용해서 접근하는 코드를 변경해야한다. 이번 포스트에서는 안드로이드 새로운 스토리지 정책을 적용한 과정을 다뤄본다.

 

1. 절대 경로 대신 Uri 를 사용하도록 변경

 

기존에는 ContentResolver 클래스를 이용해 파일을 읽어올 때 DATA 칼럼을 이용해서 파일의 절대 경로를 읽어올 수 있었다. 그런데 DATA 컬럼은 Android 10부터 Deprecated가 됐고, targetSdkVersion 30으로 올리면 DATA 칼럼으로 얻을 수 있는 절대 경로로 파일이 접근이 되지 않는다. 

 

private suspend fun loadVideoContent(): Cursor? = coroutineScope {
    val where = MediaStore.Video.VideoColumns.SIZE + " > " + 0
    val sortOrder = MediaStore.Files.FileColumns.DATE_ADDED + " DESC"
    val projections = listOf(
        MediaStore.Video.Media._ID,
        MediaStore.Video.Media.DATA, // Deprecated됨
        MediaStore.Video.Media.DISPLAY_NAME
    ).toTypedArray()

    return@coroutineScope requireActivity().contentResolver.query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, projections, where, null, sortOrder)
}

 

 

이제는 우리에게 익숙한 절대 경로 대신 Uri를 이용한 상대 경로를 사용해야한다. Uri는 content:// 로 시작하는 문자열인데, _ID 칼럼에서 얻어온 값과 ContentUri 클래스를 이용해서 얻어올 수 있다. 이 값도 파일을 찾는 경로로 사용되며 현재 Glide, MediaMetadataRetriever, Exoplayer처럼 유명한 안드로이드 라이브러리들은 Uri를 통해서도 파일을 불러올 수 있게끔 업데이트가 된 상태라 호환성은 크게 걱정하지 않아도 된다. 절대경로와 다른점은 실제 파일의 경로를 보여주지 않아 플랫폼 보안적인 요소가 강화된다. 반대로 개발자의 피로도는 악화되고.

 

val uriCol = cursor.getColumnIndex(MediaStore.Video.Media._ID)

do {
    mediaItems.add(
        MediaItem(
            ContentUris.withAppendedId(
                MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
                cursor.getLong(uriCol)
            ),

 

2. File 클래스를 선언해야하는 경우 

 

문제는 File 클래스를 선언해야하는 경우다. 오래된 오픈소스거나 Uri를 고려하지 않은 모듈인 경우엔 절대 경로가 필요한 File 클래스를 사용해야하는 경우가 종종 있다. 그런데 앞서 언급했듯이 Uri는 상대 경로다. Uri를 통해 File 클래스로 바꿀수 있긴 한데 이건 file 스키마를 가진 Uri인 경우에만 그렇다. Uri 클래스에 Kotlin에 확장 코드로 toFile() 함수가 있긴 한데 ContentUris로 얻어온 Uri 클래스에 쓰면 요런 에러가 뜬다.

 

 

이럴 때는 절대 경로를 읽을 수 있는 형태로 꼼수가 필요하다. Android 10부터 스토리지를 절대 경로로 접근하는 것은 안되지만 앱 전용 캐시 영역은 여전히 절대 경로로 접근 할 수 있다. 스토리지에 있는 파일을 캐시로 복사하면 복사한 파일의 절대 경로로 파일 클래스를 선언해줄 수 있다. copy 작업이 딜레이도 있고 불필요하게 캐시영역 써야해 완벽한 방법은 아니다. 하지만 라이브러리에서 Uri를 지원하기 전까지는 써먹을 수 있을 것 같다. 더 좋은 방법이 있다면 공유해주시면 좋겠다. 나는 이것 말고는 딱히 방법을 못찾겠다...

 

val dir = File(context.cacheDir.path + File.separator + effectFolderName)
val filePath = context.cacheDir.path + File.separator + effectFolderName + File.separator + filename
val file = File(filePath)
val inputStream = getApplication<App>().contentResolver.openInputStream(uri)

try {
    FileUtils.copyToFile(inputStream, it) // org.apache.commons.io 를 사용
} catch (e: IOException ) {
    e.printStackTrace()
}

 

3. 미디어 파일을 추가하는 경우

 

앱에서 이미지나 동영상을 다운받는 경우 예전에는 Environment.getExternalStorageDirectory().path 코드를 이용해서 직접 원하는 경로에 파일을 생성해서 추가할 수 있었으나 Android 10 부터는 ContentResolver를 이용해 Uri로 파일을 추가해야한다. 아래 코드는 이미지 파일을 저장소에 추가하는 코드다. ContentValues 값을 설정해 임의의 이미지 파일을 만든 후 insert 함수에서 생성된 Uri 변수로 FileOutputStream을 만들고 I/O 라이브러리를 이용해 기존 파일과 복사하는 작업이다. 관계형 데이터베이스에 새로운 행을 추가하고 값을 업데이트한다고 보면 쉬울 것 같다. 실제로 ContentResolver는 관계형 데이터베이스 쿼리랑 상당부분 흡사하다.

 

val collection = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)

val contentvalues = ContentValues().also {
    it.put(MediaStore.MediaColumns.RELATIVE_PATH, "Images")
    it.put(MediaStore.MediaColumns.DISPLAY_NAME, name)
    it.put(MediaStore.MediaColumns.IS_PENDING, true)
}

val uri = context.contentResolver.insert(collection, contentvalues)
val fos = context.contentResolver.openOutputStream(uri!!, "w")

try {
    FileUtils.copyFile(sourceFile, fos)
} catch (e: IOException) {
    return false
}
values.put(MediaStore.MediaColumns.IS_PENDING, false)
context.contentResolver.update(uri, values, null, null)

 

728x90

아래 그림처럼 ViewPager 형태인데 다음 페이지가 살짝 보이는 UI를 만드는 경우가 종종 있다. 이번 포스트에서는 ViewPager2를 이용해 이 화면을 만드는 방법을 다뤄보려고 한다.

 

val currentVisibleItemPx = DimensionUtils.dp2px(requireContext(), 40f).toInt()

margin_pager.addItemDecoration(object: RecyclerView.ItemDecoration() {
    override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
        outRect.right = currentVisibleItemPx
        outRect.left = currentVisibleItemPx
    }
})

ViewPager에서 inflate 된 페이지는 부모의 width를 따라가게 되므로 우선 각 페이지가 전체 영역을 잡지 않게 여백을 만들어둔다. 현재 보여진 위치로부터 양 옆에 margin을 추가한다. 그러면 아래 그림과 같은 상태가 된다.

 

 

val nextVisibleItemPx = DimensionUtils.dp2px(requireContext(), 20f).toInt()
val pageTranslationX = nextVisibleItemPx + currentVisibleItemPx

margin_pager.offscreenPageLimit = 1

margin_pager.setPageTransformer { page, position ->
    page.translationX = -pageTranslationX * ( position)
}

 

다음은 이전 페이지와 다음 페이지에 이동 효과를 줘야한다. 먼저 첫번째는 offscreenPageLimit 값을 설정는데 이 속성은 ViewPager2가 스크린에 현재 페이지로부터 얼만큼 떨어져 있는 페이지를 미리 생성 할 것인지 설정하는 함수다. offscreenPageLimit 값이 1이고, 5번 페이지가 현재 위치라면 ViewPager2는 4, 6번 페이지도 미리 생성 해둔다.

 

그 다음은 setPageTransformer 함수를 사용하는 것이다. 이 콜백은 현재 포커싱된 page 뷰 객체를 받을 수 있고 각 page 별로 포커싱 된 페이지로부터 얼마만큼 떨어져 있는지 비율 정보를 position으로 받을 수 있다. 아래 그림에선 현재 페이지가 1번에 포커싱돼 있어서 이전 페이지인 0번 페이지는 -1f만큼 떨어져 있게 되고, 다음 페이지인 2번 페이지는 1f만큼 떨어져 있게 된다. 

 

 

이 정보 값을 이용하면 0번과 2번 페이지를 필요한 만큼 이동시킬 수 있다. translationX 값을 변경하면 0번과 2번 페이지가 움직여서 미리보기 형태로 볼 수 있게 된다.

 

728x90

움직이는 TextView

개발/안드로이드 2021. 5. 11. 19:57 Posted by 아는 개발자

종종 화면내에서 움직이는 TextView를 만들어야 할 때가 있다.

 

이렇게 직선형태로 움직이는 애니메이션의 경우 TranslateAnimation 클래스를 이용해서 쉽게 구현이 가능하다. 아래 코드는 새로운 TextView를 만들고 layout에 추가한 다음 애니메이션을 실행한 코드다. 주목할 부분은 TranslateAnimation 코드다.

 

CoroutineScope(Dispatchers.Main).launch {
            val movingText = TextView(requireContext()).apply {
                this.text = "움직이는 텍스트"
                this.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply {}
                this.visibility = View.INVISIBLE
                this.setTextColor(0xff141414.toInt())
            }

            danmu_layout.addView(movingText)

            movingText.post {
                val animation = TranslateAnimation(requireView().width.toFloat(), -(movingText.width.toFloat()), 0f, 0f)
                animation.duration = 3000
                animation.repeatCount = Animation.INFINITE
                animation.setAnimationListener(object: Animation.AnimationListener {
                    override fun onAnimationStart(animation: Animation?) {
                        movingText.visibility = View.VISIBLE
                    }

                    override fun onAnimationEnd(animation: Animation?) {}
                    override fun onAnimationRepeat(animation: Animation?) {}
                })
                movingText.startAnimation(animation)
            }
        }

 

TranslateAnimation 생성자 인자에서 받는 값은 fromXDelta, toXDelta, fromYDelta, toYDelta다. xml 파일로 애니메이션을 작성할 때는 퍼센테이지 값을 넣을 수 있는데, TranslateAnimation 클래스를 사용하면 픽셀 값으로 입력해야한다. 각각이 의미하는 바를 보자. 

 

public TranslateAnimation(float fromXDelta, float toXDelta, float fromYDelta, float toYDelta) {

 

fromXDelta는 현재 위치로부터 Delta만큼 x축 방향으로 이동한 지점에서 애니메이션을 시작한다. 현재 위치부터 시작하고 싶다면 0을, 다른 위치로 변경하고 싶다면 특정 값을 설정하면 된다. + 값은 오른쪽으로 이동하고 - 값은 왼쪽으로 이동한다. 같은 원리로 toXDelta는 현 위치에서 x축 방향으로 이동한 지점에서 애니메이션을 종료한다. 왼쪽으로 이동한 지점에서 종료하고 싶다면 - 값을, 오른쪽으로 이동한 값에서 종료하고 싶으면 + 값을 넣으면 된다. 앞서 소개한 코드에선 fromXDelta에선 부모 뷰의 width만큼 움직여서 화면 밖에서 시작하고, toXDelta는 현재위치에서 텍스트의 width만큼 왼쪽으로 움직이므로 화면 밖으로 사라지는 애니메이션을 만들 수 있었다. 절대적인 좌표가 아니라 아니라 현 위치로부터 상대적인 거리로 값을 입력해야 한다는 점을 주의하자. y축에서도 동일한 원리를 적용할 수 있다.

 

728x90

저장공간으로부터 파일을 읽어오는 앱을 출시한 개발자의 경우 올해 4월 15일부터 플레이스토어에서 아래 알림을 보게 될 확률이 높다..!

 

평소 같으면 이런 알림은 그냥 넘겨버렸는데 이번 공지에는 "허용된 용도 외에 모든 파일 액세스 권한에 액세스를 요청하는 앱은 Google Play에서 삭제되고, 업데이트를 게시할 수 없게 됩니다" 라는 무시무시한 경고문이 들어 있어 개발자들을 당황스럽고 잔뜩 쫄아버리게 만든다. 아니 갑자기 스토어에서 내려버리겠다고 으름장을 내다니. 그것도 5/5일부터 적용할 예정인 정책을 4/15일에 알려주는건 어처구니가 없다. 한 달 전도 아니고 3주 남짓한 시간 정도에 이걸 어떻게 모두 정리하냐는 말이다. 그래서 평소 같으면 그냥 지나쳐버린 자세히 알아보기 버튼을 클릭했다.

 

다행히 정책이 적용되는 버전은 Android 11 수준을 타겟팅하는 앱 대상이다. 만약 현재 안드로이드 앱의 타겟 SDK가 29이하라면 아직까진 안심해도 좋다. 그런데 일찍 Android 11로 업그레이드 완료했다면 모든 권한을 요청하고 있다면

 

1. 지금부터 불필요하게 권한을 요청하는 코드를 없애던지, 

 

2. 모든 권한을 요청해야하는 이유를 양식으로 제출하던지,

 

3. SDK 버전을 29로 다시 내리던지 

 

하는 선택을 해야한다. 대부분 앱이 아직 SDK 29버전을 따르고 있을 것 같아서 큰 문제는 없을 것 같은데 부지런히 SDK 30으로 업그레이드한 개발자들은 일찍 일어난 새가 모이를 먼저 먹지 못하고 구글의 깡패 같은 정책을 먼저 얻어 맞는 꼴이니 억울함이 상당할 것 같다. 공지에 따르면 코로나 19 관련 고려사항이라고 하는데 코로나랑 저장공간 권한이 무슨 관련이 있는건지.. 그러면 SDK 30 이하 버전들도 공통적으로 적용돼야하는거 아닌가. 코로나가 SDK 30을 타겟팅한 앱만 좋아하는 것도 아닌데.

 

코로나를 핑계로 둔건지 모르겠지만 아무튼 구글은 앞으로 저장 공간에 관한 권한 관리를 빡세게 할 것을 암시하고 있다. Android 10 때 야심차게 범위 저장 공간(Scoped Storage)를 내놨다가 쓰기 어렵다는 베타 개발자들의 아우성을 듣고 requestLegacyExternalStorage 옵션을 열어 줬는데 이제는 개발자 너희들도 슬슬 준비해야 할 때라고 눈치를 주는 느낌(?) 이다. 당시엔 개발자 커뮤니티의 반대가 워낙 심각해서 구글이 이 정책을 포기할 것 같았으나 포기는 커녕 플레이스토어에서 내려버릴 거라고 엄포를 내놓고 있으니 어쩔수가 없다. 그러면 처음부터 정책을 잘 좀 만들던가!

 

나도 Android 10으로 올리면서 MediaStore를 이용해 파일의 경로를 재설정 할 수 있는 연동작업을 진행했는데 너무 빡세서 연동중에 포기하고 requestLegacyExternalStorage 옵션을 키고 기억속에 묻어 뒀었다. 그때 기존 코드를 바꾸는 과정이 고통스러웠던 기억이 아직도 생생한데. 이제는 슬슬 작업을 진행해야 할 것 같다. 나중에는 연동작업에 대해서 다뤄보려고 한다.

728x90

Thread, Runnable, Callable, ThreadPool

개발/안드로이드 2021. 4. 23. 17:29 Posted by 아는 개발자

1. Thread

 

Thread 클래스는 Java 언어에서 비동기 작업시 대표적으로 사용하는 클래스다. 코틀린과 람다를 이용하면 아래와 같이 간단하게 비동기 작업 코드를 짤 수 있어서 짧은 디코딩 작업이나, 연산처리를 할 때 주로 사용된다. 

 

fun testThread() {
    val thread1 = Thread {
        Thread.sleep(1000)
        Log.d(this.toString(), "this is test thread1")
    }

    val thread2 = Thread {
        Log.d(this.toString(), "this is test thread2")
    }

    thread1.start()
    thread2.start()
}

 

그런데 Thread를 생성하는 작업은 안드로이드 밑단 운영체제에서 pthread를 생성하게 되는 작업이므로 추가적인 메모리를 할당받게 된다. 그래서 Thread를 많이 생성할수록  더 많은 메모리 공간을 차지하게되기 때문에 무한정 만들 수 없다. 

 

2. Runnable 

 

앞서 언급한 Thread를 간소화한 형태다. Thread는 클래스인 반면 Runnable은 인터페이스라서 다른 클래스를 상속하고 있는 클래스에 Runnable을 추가할 수 있고 상속 형태 없이 익명 클래스 형태로 바로 써도 된다. 실행은 run() 클래스를 호출해서 가능하다. 

 

fun testRunnable() {
    val runnable = Runnable { Log.d(this.toString(), "test runnable") }

    runnable.run()
}

 

3. Callable 

 

Runnable과 비슷하지만 Callable은 비동기 작업에서 리턴값도 줄 수 있고 결과 값을 연산하지 못하면 Exception을 발생시키기도 한다. 리턴값을 제공해야하는 비동기작업이면 Runnable 말고 Callable을 쓰면 된다. 아래 코드는 Callable에서 정수 값들을 리턴하도록 하고 각각의 합을 로그로 찍은 것이다

 

fun testCallable() {
    val callableOne = Callable { 1 }
    val callableTwo = Callable { 2 }
    val callableLog = Callable { Log.d(this.toString(), "this is callable log") }

    try {
        val merged = callableOne.call() + callableTwo.call()
        callableLog.call()

        Log.d(this.toString(), "this is callable merged: $merged")
    } catch (e: Exception) {
        e.printStackTrace()
    }
}

 

4. ThreadPool

 

앞서 설명한 Thread, Runnable, Callable 모두 쓰레드 계열이라서 개별로 메모리를 쓰게돼, 무한정 만들면 메모리 낭비가 발생한다. 그래서 자바에서는 ThreadPool이란 것으로 쓰레드를 관리할 수 있도록 했다.  ThreadPool 관리 객체는 Executors 클래스를 통해서 생성하는데 이 클래스 검색해보면 내부의 함수를 통해 다양한 방식으로 Thread를 관리할 수 있는 것을 확인 할 수 있다. 지면상 모두 설명할 수 없고, 아래 코드에서 쓰는 newFixedThreadPool 함수만 설명하면 쓰레드를 최대 4개만 만들 수 있게 하고 나머지 작업은 재사용하겠다는 듯이다. 새로운 작업을 추가해도 쓰레드는 4개 이상 생기지 않으며 나머지는 재사용되기 때문에 메모리 낭비에 대한 부담 없이 새로운 작업을 추가할 수 있다.

 

작업은 Callable, Runnable의 형태로 추가 가능하며 submit 함수로 추가한다. 이때 리턴 값은 Future 클래스로 받게되고, get() 함수를 통해 결과 값을 얻을 수 있다.

 

fun testExecutorService() {
    val cachedPool = Executors.newFixedThreadPool(4)
    val futureOne = cachedPool.submit(Callable { 1 })
    val futureTwo = cachedPool.submit(Callable { 2 })
    val printLog = cachedPool.submit(Runnable {
        Log.d(this.toString(), "something...") })

    try {
        val merged = futureOne.get() + futureTwo.get()
        printLog.get()
        Log.d(this.toString(), "merged: $merged")

    } catch (e: Exception) {
        e.printStackTrace()
    }
}

 

728x90

우분투에 최신 nodejs 설치하기

개발/nodejs 2021. 3. 28. 12:23 Posted by 아는 개발자
sudo apt-get install nodejs

 

우분투의 패키지매니저 apt 를 이용해서 Nodejs를 설치 할 수 있기는 하다. 그러나 최신 버전이 아니라 우분투 팀에서 마지막으로 테스르를 완료한 버전(현재 글 작성 시점에서는 8.x.x)을 설치돼서 자바스크립트에서 Optional Chaning으로 짠 코드가 컴파일 되지 않는 문제가 있었다. 그래서 apt 말고 다른 방법으로 설치해야한다.

 

여러가지 방법이 있겠지만 가장 좋은 방법은 nvm(node version manager)를 쓰는 것 같다. nvm을 사용하면 다양한 노드 버전에서 설치가 가능한데 최신 버전도 물론 설치가 가능하다. 

 

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash // nvm 설치
nvm install 14.4.0

 

참고로 nvm을 이용하면 현재 설치된 node의 버전을 바꿔치기도 할 수 있다. 배포후 테스트 할 때 사용하기 좋을 프로그램이 될 것 같다

 

nvm use <version-number>
728x90

'개발 > nodejs' 카테고리의 다른 글

우분투에 최신 nodejs 설치하기  (0) 2021.03.28
nodejs + postgresql  (0) 2021.01.10
nodejs + multer 파일 업로드  (0) 2021.01.07
nodejs + s3 upload/get  (0) 2021.01.07
node-schedule-tz  (0) 2021.01.07
express  (0) 2020.12.24

jitpack 이란

개발/안드로이드 2021. 3. 28. 09:54 Posted by 아는 개발자

안드로이드 개발중 open source 라이브러리를 임포트 할 때 아래 코드처럼 가이드에 jitpack 주소를 추가하라는 문구를 보는 경우가 종종 있다.

 

allprojects {
        repositories {
            jcenter()
            maven { url "https://jitpack.io" }
        }
}

 

여기서 추가한 jitpack 주소는 추가하려는 오픈소스 라이브러리를 저장하고 있는 저장소다. jitpack은 안드로이드, JVM 형태의 오픈소스 라이브러리 배포 플랫폼이다. 추가하려는 오픈소스 라이브러리 뿐만 아니라 깃허브 프로젝트에 올라온 오픈소스 프로젝트들을 저장하고 있으며 프로젝트에서 사용할 수 있게 jar, aar 형태로 빌드한 상태로 받을 수 있다. 깃헙으로 오픈소스를 배포하고 싶은 개발자 입장에선 쉬운 배포 툴이고 깃허브뿐만 아니라 BitBucket, GitLab, Gitee, Azure 같은 저장소에서 올린 오픈소스 프로젝트도 연동이 가능하니 개발한 라이브러리르 전세계 유저한테 배포하고 싶을땐 jitpack이 가장 좋은 옵션이 될 것 같다.

728x90

안드로이드 개발중 Gson과 Retrofit을 이용해 Json 데이터를 주고 받을 때 이런 에러를 보게 되는 경우가 있다 

 

Caused by: com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was BEGIN_ARRAY at line 1 column 15 path $.documents
  at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:224)
  at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.read(ReflectiveTypeAdapterFactory.java:129)
  at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:220)

 

이 에러는 서버로 부터 전달 받은 타입과 내가 예상한 타입이 맞지 않아서 발생하는 문제다. 특히 위 에러는 나는 Object가 올것으로 기대 했는데 실제 데이터가 배열인 경우다. 아래 코드를 보면 나는 검색 결과에 대한 클래스에서 documents 인자를 하나의 오브젝트로 받고 있다. 

 

interface LibraryApi {
    @GET("/v3/search/book")
    fun search(
        @Query(value = "query") query: String,
        @Query(value = "sort") sort: String?,
        @Query(value = "page") page: Int?,
        @Query(value = "size") size: Int?,
        @Query(value = "target") target: String?
    ): Single<SearchResp>
}

@Keep
data class SearchResp(val documents: BookMeta, val meta: SearchMeta)

 

그런데 실제 서버로부터 응답은 documents는 하나의 오브젝트가 아니라 배열로 날라온다. 클라이언트에서 기대한 데이터 타입이 달라서 발생하는 에러다.

 

{
  "documents": [
    {
      "authors": [
        "전석"
      ],
      "contents": "주식투자의 성공 요소 중 중요한 것은 바로 수익을 낼 수 있는 종목을 볼 줄 아는 눈을 갖는 것이다. 지금 상승하거나 혹은 상승을 준비 중인 종목들이 본격적으로 상승하기 전에, 세력은 어쩔 수 없이 흔적을 남기게 된다. 『개미대학 세력의 매집원가 구하기』는 그런 종목을 구별해 낼 줄 아는 능력을 키울 수 있게 한다. 처음 기술적 분석을 공부하다가 힘들다고 포기하는 부분이 바로 '캔들과 거래량'인데, 이것을 실전에 적용하려면 암기가 아닌 원리의 이해가",
      "datetime": "2017-0
728x90

네트워크 디버깅으로 Stetho 라이브러리와 chrome://inspect 를 이용하곤 하는데, 안드로이드 스튜디오의 Profiler를 사용해도 동일하게 네트워크 디버깅을 할 수 있다. Stetho를 오래 사용하는 경우에 안드로이드 스튜디오랑 연결된 디바이스가 끊기는 문제가 있었는데 이 방식을 이용하면 끊길 염려 없이 사용할 때 더 간편하다. 이번 포스트에서는 Profiler를 이용한 네트워크 디버깅 방법을 간단히 소개한다.

 

1. Profiler 실행 

 

View -> Tool Windows -> Profiler 로 Profiler를 실행한다. 

 

 

2. 프로파일링할 프로세스 선택 

 

현재 연결중인 디바이스에서 디버깅할 앱 프로세스를 세션으로 추가한다. 당연한 얘기지만 앱은 debug 모드로 빌드해야 프로파일링 할 수 있다. 선택을 하면 프로세스의 CPU, Memory, Network, Energy 사용량을 시간 순서로 볼 수 있다.

 

 

 

3. 네트워크 프로파일링 선택 

 

그래프에서 네트워크를 선택하고 앱에서 네트워크 요청을 보내보면 아래 그림처럼 파란 직선 그려진 트래픽을 볼 수 있다. 마우스를 이용해 파란색 직선 영역을 블록처리해보면 해당 구간에서 주고 받은 네트워크 요청 목록을 볼 수 있다. 

 

 

4. 네트워크 디버깅 

 

디버깅하려는 네트워크 요청을 클릭해보면 오른쪽 탭에 상세 요청과 응답을 볼 수 있다. Body가 Json 형태인 경우 깔끔하게 그려준다. chrome://inspect에 비해 좋은 점은 Body에서 에디팅이 쉽다. chrome의 경우에는 복사를 하는 경우 디버깅 화면이 꺼지는 버그가 있었는데 Profiler를 사용하면 이런 버그가 없이 일반 코드에서 에디팅 할 때랑 똑같다.

 

728x90

'개발 > 안드로이드' 카테고리의 다른 글

Thread, Runnable, Callable, ThreadPool  (0) 2021.04.23
jitpack 이란  (0) 2021.03.28
안드로이드 스튜디오를 이용한 네트워크 디버깅  (0) 2021.03.14
RoundedFrameLayout  (0) 2021.03.03
겹치는 recyclerview 만들기  (0) 2021.02.15
android - Hilt 사용기  (0) 2021.01.15

Node.js vs Spring Boot

개발/spring 2021. 3. 13. 22:32 Posted by 아는 개발자

 

현재 서버 애플리케이션 플랫폼의 큰 두 축은 Spring Boot 와 Node.js 인것 같다. 각각의 플랫폼마다 고유한 장점이 있을 텐데 정작 나는 '일하고 있는 곳에서 사용중이다', '요새 이게 트렌드라고 한다'는 이유로 본질을 망각한채 공부만 해왔던 것 같다. 그래서 이번 포스트에서는 spring boot와 nodejs 각각의 장점과 단점을 늦었지만 다뤄 보고자 한다.

 

Node.js

 

Node.js하면 자바스크립트로 짤 수 있는 서버 애플리케이션을 가장 먼저 떠오르는데 사실 Node js는 Non-blocking I/O를 처리하는데 최적화된 플랫폼이다. Non-blocking I/O는 다른 작업이 처리되는 걸 기다리는 도중에 다른 작업을 하는 것을 말하며 이러한 형태는 짧은 시간에 여러 작업을 처리할 수 있어 효율적이다. 다른 언어에서도 이런 형태로 구현은 가능하지만 코드가 너무 지저분해지고 구현이 어려운 단점이 있었는데 Node js에서는 비동기식 함수를 통해 코드 상에서 이 작업을 구현하기 간편하게 만들어줬다. 실제로 최근에 만든 사이드프로젝트에서 Non-blocking I/O를 구현하는게 정말 간편했다. 그리고 내부적으로는 하나의 Thread를 이용해서 구현했기 때문에 메모리를 크게 잡아 먹지도 않아 효율적이다. 똑같은 애플리케이션을 돌려도 다른 프레임워크보다 Node.js가 소모하는 메모리의 크기가 적다.

 

단점은 JavaScript 언어를 사용한다는 점이다. JavaScript가 배우기는 참 쉬워서 적은 시간을 투자하고 금방 숙달을 할 수 있으나 프로젝트 규모가 커지면 커질수록 Type Safe 하지 못하는 점이 한계점으로 작용한다. 언어가 Type Safe 하지 못하면 내가 짠 코드가 별것도 아닌 에러로 런타임에 죽을 수도 있다. 대부분 이 에러는 Java나 C언어 를 사용했다면 빌드 중에 발생하는 컴파일 에러 종류인데 JavaScript는 빌드하는 과정이 없기 때문에 실행 전에 잡아 주질 못한다. 구현하고 서버 실행까지 매우 빠르다고 좋아 할지 모르나 이 사이에 컴파일 오류는 없을 지 꼼꼼히 봐야한다. 그리고 Type Safe하지 못해서 IDE에서 자동 완성이 잘 되지 않는다. 프로젝트가 커지면 커질수록 리팩토링을 하거나 기존 코드를 써먹어서 확장해야 할 때 자동완성 기능이 핵심인데 JavaScript를 쓰면 자동완성이 잘 안돼서 큰 애를 먹게 된다. 프론트엔드 프레임워크 React에서는 TypeScript를 도입해서 어느정도 보완하고 있는데 Node.js에서도 TypeScript를 도입하는 시도가 있다고 들었는데 어느 정도 진행됐는지 모르겠다. 

 

Spring Boot

 

SpringBoot는 Java로 만든 서버 애플리케이션이다. Java는 유구한 역사를 가지고 있고 지금도 많이 사용되는 언어라 스프링부트를 사용하면 Java 언어에서 있는 기능을 그대로 사용할 수 있다. Java를 개발해본 사람들은 쉽게 Spring Boot에 적응 할 수 있다. 그리고 역사가 오래 됐기 때문에 개발하는데 필요한 왠만한 라이브러리는 모두 Spring Boot에 다 있다. 안드로이드 개발자가 사용한 자바 라이브러리들은 모두 Spring에서도 찾을 수 있다고 볼 수 있고 추가로 서버 개발자들이 어려움을 겪는 데이터베이스 관리도 스프링부트에서는 JPA라는 라이브러리를 통해 간소화 해둬서 손쉽게 다룰 수 있다. 그리고 Java이기 때문에 TypeSafe 하다. 리팩토링하거나 확장 할 때 IDE를 이용해서 수정할 점을 빠르게 체크 할 수 있는데 프로젝트 규모가 커지고 안정성이 중요해지는 시점부터는 큰 장점으로 다가온다. 내부적으로는 Multi Threading을 지원하는 구조로 짜여있어서 길고 반복적인 업무를 처리할 때 효율적이다. 많은 양의 컴퓨팅이 필요한 경우 잘 써먹으면 좋다.

 

한번 써보신 분들은 알겠지만 Spring Boot는 러닝 커브가 존재한다. Node.js는 처음 배우는 사람도 하루만에 서버 구동하고 api도 하나 만들 수 있는데 Spring Boot를 공부하면 Service, Controller, Repository 에 대해서 알아야하고 각 컴포넌트는 어떤식으로 채워야하는지 공부가 필요해 해야 할 게 많다. Spring Boot에서는 좋은 구조를 유도하기 위해 이런 형태의 디자인을 권장하는데 초심자한테는 러닝 커브가 좀 있다. 그리고 boilerplate 코드가 많다. 스프링에서 권장하는 구조랑 라이브러리들을 사용하려면 이런 저런 코드를 만들어야 하는데 처음에는 어려우나 숙달되면 귀찮아진다. 그래도 안쓰는 것 보다 낫긴 하지만. 내부적으로는 메모리를 좀 많이 쓴다. Multi thread 환경이기 때문에 여러개의 Thread를 띄우다 보니까 어쩔 수 없이 생긴 문제인 것 같다. 

 

728x90

'개발 > spring' 카테고리의 다른 글

Node.js vs Spring Boot  (5) 2021.03.13
Spring 테이블 칼럼이 아닌 필드 데이터 받아오기  (0) 2021.03.05
  1. 지나가는 나그네 2021.03.19 18:50  댓글주소  수정/삭제  댓글쓰기

    Node.js를 프레임워크라고 하지 않습니다. javascript runtime 입니다.
    그래서 spring boot 와 비교하기 위해서는 node 에서 동작하는 다른 프레임워크와 비교해야 합니다.

  2. Werewolf 2021.04.17 11:21  댓글주소  수정/삭제  댓글쓰기

    요즘 많이 쓰이는 대부분의 Node.js 기반 프레임워크는 타입스크립트를 지원하고 있습니다. 이 중에서도 Nest.js 의 경우 프레임워크 자체가 타입스크립트로 개발되었고 스프링과 매우 흡사한 구조를 가지고 있기 때문에 기존의 스프링 개발자 분들이나 혹은 Node.js 개발자 분 중 스프링 프레임워크로 넘어갈 걸 고려하는 개발자 분들에게 매우 좋은 선택인 것 같습니다.

    어떻게 보자면 스프링 프레임워크가 혹독한 다이어트를 거쳐 체중을 감량하고 이벤트 루프 기반 비동기 I/O를 기본으로 지원하도록 변형된 버전을 Nest.js라고 보면 될 것 같습니다. 당근마켓 등 스타트업 몇 곳에서 Nest.js를 활용하고 있고 근래의 Node.js 기반 프레임워크 중에서는 나름 대세(?)라고 해도 과언이 아닐 것 같네요.

Spring으로 쿼리를 만들다보면 여러개의 테이블을 조인한 쿼리에서 다른 테이블 칼럼의 값까지 읽어올 필요가 있다. 예를 들면 글정보를 받아 오는 api가 있는데 내가 그 글을 좋아요 했는지, 안했는지 유무까지 알려주는 요구 사항의 경우 두 개의 테이블을 조인해야한다.

 

쿼리문을 짜면 다음과 같다. tb_post에 있는 모든 필드를 가져오고, 좋아요 유무는 liked 필드 이름으로 받아오는 것으로 뒀다.

select tb_post.*, when tb_post_user_like.post_id > 0 then true else false end as liked from tb_post
left join tb_post_like on (tb_post.post_id = tb_post_user_like.post_id and tb_post_like.user_id = :findUserId)
where tb_post.post_id = :postId

 

JPA에서 제공하는 CrudRepository 로는 이미 있는 테이블의 칼럼을 매핑해서 받아오는데 반해 이 방법은 임의로 liked라는 새 칼럼을 생성한 것이기 때문에 테이블과 1:1 매핑된 엔티티 클래스에서 했던 것처럼 칼럼을 자동으로 매핑하는게 안되고 다른 방법을 써야한다. 열심히 구글 검색을 해본 결과 세가지 방법을 발견했다. 

 

1. Object형태로 받아오기 

Query문에서 리턴 값을 Object로 받아오면 모든 필드 값의 리턴을 받아 올 수 있다. 가장 직관적이고 쉬운 방법이다.

 

@Query(value = "select tb_post.*, case when tb_post_like.post_id > 0 then true else false end as liked from tb_post \n" +
        "from tb_post \n" +
        "left join tb_post_like on (tb_post.post_id = tb_post_like.post_id and tb_post_like.user_id = :findUserId)\n" +
        "where tb_post.post_id = :postId", nativeQuery = true)
fun findPostById(@Param("postId") postId: Long, @Param("findUserId") findUserId: Long) : List<Array<Any>>

하지만 이렇게 읽어오면 아래 그림처럼 필드 값이 생략돼서 날라오게 돼서 알아보기가 힘들다. 쿼리문에서 칼럼 필드 순서를 지정하는 방법으로 처리할 수 있으나 알아보기가 힘들어서 관리하기가 어려운 단점이 있어 추천하지 않는다. 위와 같은 형태로 읽어오는 클래스를 만든다면 더더욱 쓰지 않는게 좋다.

 

 

2. JPA New 명령어 

 

JPA 쿼리의 New 명령어를 사용하면 리턴 값을 클래스로 줄 수 있다. 단 이 방법은 native query 문을 사용하지 못하고 jpa 쿼리를 사용해야 한다는 점이다. limit을 쓰는 구문에서는 사용할 수 없다.

 

data class Post(postId: Long, postTitle: String, postContent: String, liked:Boolean)

@Query(value = "select new com.package.Post(tb_post.post_id, tb_post.post_title, tb_post.post_content, case when tb_post_like.post_id > 0 then true else false end) from tb_post from tb_post left join tb_post_like on (tb_post.post_id = tb_post_like.post_id and tb_post_like.user_id = :findUserId) where tb_post.post_id = :postId")
fun findPostById(@Param("postId") postId: Long, @Param("findUserId") findUserId: Long) : List<Post>

 

3. SetQueryResultSetMapping 

 

쿼리에서 읽어온 컬럼 필드를 클래스에 매핑 해줄 수 있는 어노테이션이다. ConstructorResult 어노테이션에서 칼럼 필드 값을 읽어와서 Post 클래스의 생성자로 만들 수 있다. 하단의 NamedNativeQuery 어노테이션에서는 쿼리의 이름을 정하고, ConstructorResult에서 참조하는 필드의 형태로 읽어 올 수 있도록 Select 문을 만들어 주고 사용할 mapping을 SqlResultSetMapping에서 지정한 이름과 동일한 값을 입력한다. 이렇게 하면 이 쿼리는 자동으로 Post 클래스 값을 출력하는 쿼리가 된다.

 

@SqlResultSetMapping(
        name = "PostMapping",
        classes = [
                ConstructorResult(
                        targetClass = Post::class,
                        columns = [
                                ColumnResult(name = "post_id", type = Long::class),
                                ColumnResult(name = "post_title", type = String::class),
                                ColumnResult(name = "post_content", type = String::class),
                                ColumnResult(name = "liked", type = Boolean::class)
                        ]
                )
        ]
)
@NamedNativeQueries(value = [
    NamedNativeQuery(name = "findPostByIdBaseOnUser", query = "select tb_post.post_id, tb_post.post_title, tb_post.post_content, case when tb_post_like.post_id > 0 then true else false end as liked from tb_post \n" +
            "left join tb_post_like on (tb_post.post_id = tb_post_like.post_id and tb_post_like.user_id = :findUserId)\n" +
            "where tb_post.post_id = :postId resultSetMapping = "PostMapping")
])
@Entity
data class Post(
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        val postId: Long,
        @Column
        val postTitle: String,
        @Column
        val postContent: String,
        @Column
        val liked: Boolean = false
): Serializable

 

아래의 코드로 쿼리 호출이 가능하다. 앞서 설저한 NamedNativeQuery의 이름 값과 동일하게 넣는다. 초반에 보일러플레이트 코드가 많지만 Post 클래스를 지속적으로 사용하고자 한다면 필드 값을 참조 할 수 있기 때문에 관리가 더 편리하다. 

 

@Service
class PostService {
    @PersistenceContext
    lateinit var entityManager: EntityManager

    fun postById(postId: Long, findUserId: Long): Post? {
        return entityManager.createNamedQuery("findPostByIdBaseOnUser", Post::class.java)
                .setParameter("postId", postId)
                .setParameter("findUserId", findUserId)
                .resultList
                .firstOrNull()

    }
}
728x90

'개발 > spring' 카테고리의 다른 글

Node.js vs Spring Boot  (5) 2021.03.13
Spring 테이블 칼럼이 아닌 필드 데이터 받아오기  (0) 2021.03.05

RoundedFrameLayout

개발/안드로이드 2021. 3. 3. 13:26 Posted by 아는 개발자

레딧의 투데이 피드탭

디자이너와 협업하다보면 위 그림처럼 이미지의 꼭지점 부분에 radius를 넣어야하는 경우가 종종 생긴다. 아이콘으로 넣는 이미지의 경우에는 디자이너가 직접 아이콘의 radius를 먹일 수 있는데 뉴스피드처럼 외부에서 받아오는 이미지의 경우에는 매번 작업을 할 수 없어 코딩으로 처리해야한다. 이럴 경우 RoundedFrameLayout 라이브러리를 사용하면 쉽게 처리가 가능하다.

 

1. 라이브러리 설치 

 

build.gradle에 추가해서 적용한다.

 

dependencies {
    // RoundedFrameLayout
    compile 'com.github.QuarkWorks:RoundedFrameLayout-Android:0.3.7'
}

 

2. 적용 

 

RoundedFrameLayout은 이미지가 적용되는 ImageView의 부모로 설정한다. 뷰의 속성 값으로 cornerRadius가 있는데 이 값을 이용해서 얼마나 깎을 것인지 적용 할 수 있다. ImageView는 부모 레이아웃이 변경됐으므로 자동으로 적용되게된다.

 

<com.quarkworks.roundedframelayout.RoundedFrameLayout
    android:id="@+id/layout_rounded_image"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:cornerRadiusTopLeft="10dp"
    app:cornerRadiusTopRight="10dp"
    app:cornerRadiusBottomLeft="10dp"
    app:cornerRadiusBottomRight="10dp">
    <ImageView
        android:id="@+id/layout_rounded_image_iv"
        android:background="#0c000000"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="centerCrop"/>
</com.quarkworks.roundedframelayout.RoundedFrameLayout>
728x90

'개발 > 안드로이드' 카테고리의 다른 글

jitpack 이란  (0) 2021.03.28
안드로이드 스튜디오를 이용한 네트워크 디버깅  (0) 2021.03.14
RoundedFrameLayout  (0) 2021.03.03
겹치는 recyclerview 만들기  (0) 2021.02.15
android - Hilt 사용기  (0) 2021.01.15
item decoration  (0) 2020.12.06

겹치는 recyclerview 만들기

개발/안드로이드 2021. 2. 15. 17:44 Posted by 아는 개발자

서비스 개발 하다 보면 위 그림처럼 recyclerview인데 아이템을 겹치는 형태로 만들어야 할 때가 있다. 먼저 쉽게 생각해 볼 수 있는 방법은 ItemDecoration을 이용해 item1을 제외한 item2, item3의 left 오프셋을 왼쪽으로 당겨주는 방법이 있다.

 

rv.addItemDecoration(object: RecyclerView.ItemDecoration() {
    override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
        val position = parent.getChildAdapterPosition(view)
        if (position != 0) outRect.left = DimensionUtils.dp2px(requireContext(), 10f).toInt() * -1  
    }
})

 

그런데 이렇게 만들면 예상했던 것과 다르게 뒤에 있는 아이템이 앞에 있던 아이템 위로 올라가게 된다. 뒤에 있는 아이템을 우선순위를 높게 쳐서 발생하는 에러다.

 

 

처음에 계획했던 대로 만들려면 recyclerview 에 약간 트릭을 추가해야한다. 사용한 LinearLayoutManager에서 reverseLayout과 stackFronEnd 속성 값을 true로 설정한다. reverLayout을 true로 두면 아이템을 RTL에 맞춰서 오른쪽으로 쌓는 것이고, stackFronEnd는 recyclerview 영역의 끝부분부터 채우는 것이다. item을 역순으로 출력할 것이므로, 맨 앞에 있는 것은 맨 뒤로 가기 때문에 offset 설정 함수도 끝 부분이 이동하도록 바꿔준다.

 

rv.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false).apply {
    reverseLayout = true
    stackFromEnd = true
}

rv.addItemDecoration(object: RecyclerView.ItemDecoration() {
    override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
        val position = parent.getChildAdapterPosition(view)
        if (position != (adapter?.itemCount?: 0) - 1) {
            outRect.left = DimensionUtils.dp2px(context, 10f).toInt() * -1
        }
    }
})

 

 

위 코드로 설정하면 아래와 같은 그림이 나온다. 예상했던 그림이긴 한데, item 순서가 역순이다.  해결방법은 간단하다. rv의 adapter에 item을 넣을 때 역순으로 넣으면 된다.

 

adapter?.submitItems(it.reversed())

 

결과 이렇게 겹치는 recyclerview 아이템을 볼 수 있다.

 

728x90

git 에 올라간 파일 이름 확인하기

개발/삽질 기록 2021. 1. 26. 20:07 Posted by 아는 개발자

이전 포스트에서 맥에서 파일의 대소문자 구분을 하지 않는 특성 때문에 크게 한번 삽질한 적이 있었는데 얼마 지나지 않아서 같은 삽질을 반복하고 말았다... 내 로컬 저장소에서는 아무 문제 없이 돌아가는데 리눅스 기반 heroku에서는 파일을 찾지 못하는 에러를 또 보고 말았다.. 부글부글.

 

이번에는 깃허브에서 관리하는 프로젝트도 아니라서 무슨 파일이 잘못됐는지 찾기도 어려웠는데 다행히 명령어 한줄로 긴 삽질을 막을 수 있었다. 깃에 올라간 파일 이름을 리스트로 출력할 수 있다.

 

user@kwony mytrot-admin % git ls-files
.env.development
.env.production
.gitignore
README.md
package.json
public/favicon.ico
public/index.html
public/logo192.png
public/logo512.png
728x90

android - Hilt 사용기

개발/안드로이드 2021. 1. 15. 14:29 Posted by 아는 개발자

예전에 쓴 Hilt 포스트에선 기존에 사용중인 프로젝트에 Hilt를 쉽게 적용할 수 없어 아쉽다는 점을 다루었다. 그래서 최근에 소소하게 시작한 사이드프로젝트에선 처음부터 Hilt를 도입해서 사용해봤다. 확실히 Dagger에 비해 자유롭고 사용하기가 간편했다. 이번 포스트에서는 어떤점이 좋았는지를 다뤄보고자 한다. 

 

1. private val 변수 형태로 주입 가능.

 

Dagger로 의존성을 주입할 때는 @Inject 어노테이션과 뒤에 lateinit var 을 붙여줘야했다. 그런데 앞으로 바뀌지 않을 변수에 var 형태로 선언하는게 여간 찝찝한게 아니었다. 다행히 Hilt에서는 이런 찝찝함을 해결했다. 생성자의 인자로 추가해 의존성을 주입할 수 있어 값이 변경되지 않은 val 형태로 주입이 가능하다. 아래 코드는 @ViewModelInject 어노테이션을 이용해 module에서 선언된 객체들에 바로 의존성을 주입하는 코드다. private 변수로도 주입이 가능하다.

 

class AssetEditorViewModel @ViewModelInject constructor(
    @Assisted private val savedStateHandle: SavedStateHandle,
    application: Application,
    private val assetRepository: AssetRepository,
    private val assetTypeRepository: AssetTypeRepository
): AndroidViewModel(application) {

}

@Module
@InstallIn(ApplicationComponent::class)
class DatabaseModule {
    ...

    @Singleton
    @Provides
    fun provideAssetRepository(appDatabase: AppDatabase) = AssetRepository(appDatabase.assetDao())

    @Singleton
    @Provides
    fun provideAssetTypeRepository(appDatabase: AppDatabase) = AssetTypeRepository(appDatabase.assetTypeDao())
}

 

물론 activity, fragment 처럼 생성자를 customize 할 수 없는 클래스도 있다. 이런 경우 기존과 동일하게 lateinit var를 붙인 채로 주입이 가능하다.

 

@AndroidEntryPoint
class MainFragment : BaseFragment(R.layout.fragment_main) {

    @Inject lateinit var assetRepository: AssetRepository

 

2. ViewModel 의존성 주입이 쉽다

 

Dagger에서는 ViewModel 을 공식적으로 지원해주는게 아니어서 별도의 Factory 클래스를 만들어서 주입을 해줘야 했다. 예로 Fragment를 만들면 이 Fragment Module에선 주입할 ViewModel을 팩토리 형태로 만들어줘야하고 ViewModelMap에 따로 등록도 해줘야하고 결과적으로 코드가 너무 늘어나 관리가 어렵다. Hilt에서는 ViewModel 의존성 주입을 공식적으로 지원해주기 시작했다.

 

ViewModel은 @ViewModelInject 어노테이션을 생성자 앞에 붙이고 ViewModel에서 사용하려는 의존성 주입 클래스를 선언만 하면 된다. Activity, Fragment 단에서는 코틀린 delegate 속성인 by viewModels(), by activityViewModels()를 통해 ViewModel을 받으면 평소와 동일하게 사용할 수 있다.

 

@AndroidEntryPoint
class MainActivity : BaseActivity() {
    private val mainViewModel: MainViewModel by viewModels()
}

@AndroidEntryPoint
class AssetsFragment: Fragment(R.layout.fragment_assets) {
    private val mainViewModel: MainViewModel by activityViewModels()
}

class MainViewModel @ViewModelInject constructor(
    @Assisted private val savedStateHandle: SavedStateHandle,
    application: Application,
    private val accountRepository: AccountRepository,

 

3. Module 만들고 등록 할 필요가 없다.

 

Dagger에서는 어떤 Module을 만들면 Dagger에 등록해주는 Module에다가 추가해야했다. 그래서 열심히 Module을 만들어도 추가하는 작업을 빼먹어으면 런타임시 에러가 수두룩 뜨곤 했었다. 근데 Hilt에서는 따로 추가하는 작업 없이 @InstallIn 어노테이션만 추가해주면 된다. 귀찮고 빼먹기 쉬운 코드를 확 줄일 수 있었다.

 

@Module
@InstallIn(ApplicationComponent::class)
class DatabaseModule {
    @Singleton
    @Provides
    fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase {
        return Room.databaseBuilder(context, AppDatabase::class.java, "database")
            .build()
    }

 

이외에도 편리한 점이 더 많을텐데 사이드 프로젝트 규모가 크지 않아서 아직 다 경험하지 못한 것 같다... 앞으로 쓰다가 괜찮으면 추가로 정리해서 올려야지.

728x90

nodejs + postgresql

개발/nodejs 2021. 1. 10. 13:12 Posted by 아는 개발자

nodejs로 postgresql 데이터베이스를 사용하는 방법. 엄청 간단하다. 

 

먼저 pg 라이브러리를 npm으로 설치한 후

 

npm install pg // pg library install

 

host 주소랑 포트번호 그리고 유저 정보들을 담은 오브젝트를 만든 후 pg client를 생성해 연결을 시켜준다.

 

const dbconfig = {
    host: process.env.DB_HOST,
    user: process.env.DB_USER,
    password: process.env.DB_PW,
    database: process.env.DB_NAME,
    port: process.env.DB_PORT,
    ssl: {
        rejectUnauthorized: false
    }
}

const client = new pg.Client(dbconfig)

client.connect(err => {
    if (err) {
        console.log('Failed to connect db ' + err)
    } else {
        console.log('Connect to db done!')
    }
})

 

정상적으로 연결이 완료 되면 선언한 pg client 객체를 이용해 db 쿼리를 날린다. 결과 값은 promise의 형태로도 받을 수 있는데 여기선 비동기 콜백을 피하고자 await로 받았다. 쿼리 결과 값은 리턴 객체의 rows 배열에 있으니 얘를 잘 써먹으면 된다.

 

rows() = () => client.query('select * from tb_table')

router.get('/api/v1/rows', async (req, res) => {
    try {
        const rowQuery = await rows();
        const resp = response.Builder.buildOkResponse({
            row: rowQuery.rows.map()
        })

 

 

728x90

'개발 > nodejs' 카테고리의 다른 글

우분투에 최신 nodejs 설치하기  (0) 2021.03.28
nodejs + postgresql  (0) 2021.01.10
nodejs + multer 파일 업로드  (0) 2021.01.07
nodejs + s3 upload/get  (0) 2021.01.07
node-schedule-tz  (0) 2021.01.07
express  (0) 2020.12.24

cannot find module - heroku

개발/삽질 기록 2021. 1. 10. 11:01 Posted by 아는 개발자

파일 이름을 리팩토링 한 후 새롭게 배포를 했더니 heroku에서 파일을 찾을 수 없다는 에러가 발생하게 됐다. 분명 로컬에서는 아무 문제 없이 제대로 돌아가고 있는데 heroku에 deploy하면 file을 찾을 수 없다는 에러가 발생했다.

 

2021-01-10T01:25:18.946380+00:00 app[web.1]: mytrot development mode
2021-01-10T01:25:19.123710+00:00 app[web.1]: internal/modules/cjs/loader.js:883
2021-01-10T01:25:19.123711+00:00 app[web.1]: throw err;
2021-01-10T01:25:19.123712+00:00 app[web.1]: ^
2021-01-10T01:25:19.123712+00:00 app[web.1]:
2021-01-10T01:25:19.123713+00:00 app[web.1]: Error: Cannot find module '../utils/errors'
2021-01-10T01:25:19.123713+00:00 app[web.1]: Require stack:
2021-01-10T01:25:19.123713+00:00 app[web.1]: - /app/lib/middleware/auth-checker.js
2021-01-10T01:25:19.123714+00:00 app[web.1]: - /app/lib/routers/user.js
2021-01-10T01:25:19.123714+00:00 app[web.1]: - /app/index.js
2021-01-10T01:25:19.123714+00:00 app[web.1]: at Function.Modu

 

하지만 아무리 눈 씻고 찾아봐도 내 프로젝트상의 코드에는 문제가 없다. 파일을 임포트 한 파일 명도 문제 없고 파일도 지정된 경로에 있다.

 

 

이럴때는 heroku app 의 터미널을 열어본다. heroku run bash 명령어로 나의 애플리케이션 프로젝트 폴더의 터미널을 열어 볼 수 있다. 문제가 된 파일이 있는 경로로 이동하니까 파일 이름이 Errors.js 라고 돼있었다. Errors.js는 errors.js 전에 설정한 파일 명이다. 파일 이름 변경사항이 heroku 프로젝트에 반영이 되지 않아서 그렇다.

 

user@kwony ~ % heroku run bash -a mytrot-dev
~ $ cd lib/utils/
~/lib/utils $ ls
Errors.js  jwt-utils.js  trot-response.js

 

heroku의 문제라기 보다는 mac + git의 문제라고 보는게 맞다. 문제가 된 파일(Errors.js, errors.js)은 대소문자가 바뀐 것 빼고는 변경 사항이 없는데 mac의 git에서는 이런 파일 이름의 변경사항을 놓치고 있다.

 

파일 이름을 index.js 에서 Index.js로 바꿨는데 git status에서는 변화 없는 것으로 나온다.

 

이럴때는 파일 이름을 대소문자만 바꾸는게 아니라 다른 변경 사항도 추가한다거나, 아니면 파일 이름을 한단계 더 거쳐서 바꾸거나 아니면 git mv 로 일일이 파일 이름을 바꾸면 된다. 번거롭고 실수하기도 쉬운 부분인데.. 왜 이렇게 만들어놨지

 

mv Errors.js errors-temp.js
mv errors-temp.js errors.js

----------------------------------

git mv -f OldFileNameCase newfilenamecase
728x90

nodejs + multer 파일 업로드

개발/nodejs 2021. 1. 7. 20:07 Posted by 아는 개발자

multer 라는 npm 라이브러리를 사용하면 nodejs로 쉽게 파일 업로드 api를 구축 할 수 있다. multer 라이브러리를 설치하고 내부 함수인 diskStorage 로 저장 받는 파일의 장소와 저장하게될 파일 이름을 설정하자. destination 필드에 파일 다운 받을 경로를 정하고 filename 필드에 다운 받는 파일 이름을 정한다. 그리고 정해둔 값으로 multer 오브젝트를 생성한다.

 

const multer = require('multer')
const path = require('path')

const storage = multer.diskStorage(
    {
        destination: './uploads',
        filename: function (req, file, cb) {
            cb(null, Date.now() + path.extname(file.originalname))
        }
    }
)

const upload = multer( { storage: storage } )

 

api 에서는 마지막에 생성한 multer 오브젝트를 middleware로 넣어주는데 파일 필드로 받을 key 값을 같이 넣어준다. 아래 api에서는 image라는 필드로 정했다. 이렇게 api를 설정하고 post 명령으로 호출하면 uploads 폴더에 파일이 생긴다.

 

router.post('/media/image', upload.single('image'), async(req, res) => {
    try {
        res.status(200).send()
    } catch (error) {
        console.log(error)
        res.status(400).send(error)
    }
})

 

 

728x90

'개발 > nodejs' 카테고리의 다른 글

우분투에 최신 nodejs 설치하기  (0) 2021.03.28
nodejs + postgresql  (0) 2021.01.10
nodejs + multer 파일 업로드  (0) 2021.01.07
nodejs + s3 upload/get  (0) 2021.01.07
node-schedule-tz  (0) 2021.01.07
express  (0) 2020.12.24

nodejs + s3 upload/get

개발/nodejs 2021. 1. 7. 19:56 Posted by 아는 개발자

nodejs로 AWS S3 스토리지에 업로드를 하기 위해선 먼저 S3 access 권한을 갖고 있는 IAM 사용자가 있어야 한다. 이것까지 설명하면 어려우니 아래 사진과 같은 권한을 가진 사용자가 필요하다는 것을 먼저 알아두자. 

 

 

사용자를 만들면 accessKeyId랑 secretAccessKey를 받는다. 이 string 값을 이용해 nodejs의 AWS S3 오브젝트를 만든다

 

const AWS = require('aws-sdk')

const s3 = new AWS.S3({
    accessKeyId: process.env.S3_ACCESS_KEY_ID,
    secretAccessKey: process.env.S3_SECRET_ACCESS_KEY
})

 

만든 객체로 s3 고유 함수를 호출 할 수 있다. 업로드의 인자에는 s3 버킷을 식별 할 수 있는 정보인 버킷 이름(Bucket), 권한 정보(ACL), 컨텐츠 타입 (ContentType), 보낼 파일 스트림 (Body), 파일 저장 경로(Key)를 입력한다. 아래 코드는 sync하게 호출하고자 promise() 함수까지 호출 했는데 원래는 비동기 함수라 .then, catch 함수도 호출이 가능하다.

 

const createUploadParam = (filePath) => {
    return {
        'Bucket': process.env.S3_BUCKET_NAME,
        'ACL': 'public-read',
        'ContentType': 'image/' + path.extname(filePath).substring(1),
        'Body': fs.createReadStream(filePath),
        'Key': moment().format("YYYY-MM-DD") + '/' + path.parse(filePath).base
    }
}

exports.uploadImage = async (filePath) => {
    return await s3.upload(createUploadParam(filePath)).promise()
}

 

파일 가져 올 때도 비슷하게 버킷의 정보가 필요하다. 단 이때는 버킷 이름과, 파일 경로만 있으면 된다. headObject 함수로 파일의 유무를 확인 한 후 있으면 getObject 함수로 파일에 대한 stream을 받을 수 있다

 

exports.createReadParam = (filePath) => {
    return {
        'Bucket': process.env.S3_BUCKET_NAME,
        'Key': filePath
    }
}

s.headObject(createReadParam(filePath)).on('success',() =>{
    s.getObject(param).createReadStream().pipe()
})
728x90

'개발 > nodejs' 카테고리의 다른 글

nodejs + postgresql  (0) 2021.01.10
nodejs + multer 파일 업로드  (0) 2021.01.07
nodejs + s3 upload/get  (0) 2021.01.07
node-schedule-tz  (0) 2021.01.07
express  (0) 2020.12.24
debugger  (0) 2020.12.24

node-schedule-tz

개발/nodejs 2021. 1. 7. 19:28 Posted by 아는 개발자

node-cron 이란 라이브러리를 사용하면 몇 시간이나 몇 일을 주기로 특정 작업을 반복해서 실행 할 수 있지만 내가 원하는 시간대에 실행하는 것은 어렵다. 예로 매일 23시 55분에 실행하는 작업을 만드려면 cron 실행을 23시 55분부터 실행하도록 하거나 아니면 1분 주기로 재실행해서 23시 55분이 지났는지 확인해야하는데 이건 번거롭다.

 

이럴때는 node-schedule이란 라이브러리를 이용하면 된다. 이 라이브러리는 특정 작업을 언제 실행할 것인지 설정 할 수 있다. cron과 거의 비슷한데 내가 작업 시간을 설정 할 수 있다는 점에서 다르다. 

 

아래 코드는 매시 42분 마다 실행되는 코드다. 2시 42분, 3시 42분마다 아래 코드가 실행된다.

 

var schedule = require('node-schedule');
 
var j = schedule.scheduleJob('42 * * * *', function(){
  console.log('The answer to life, the universe, and everything!');
});

 

scheduleJob 첫번째 인자에 들어가는 위치별로 분, 시, 일, 월, 요일을 정해줄 수 있다. 모두 포함하고 싶으면 별표를 넣으면 된다.

 

*    *    *    *    *    *
┬    ┬    ┬    ┬    ┬    ┬
│    │    │    │    │    │
│    │    │    │    │    └ day of week (0 - 7) (0 or 7 is Sun)
│    │    │    │    └───── month (1 - 12)
│    │    │    └────────── day of month (1 - 31)
│    │    └─────────────── hour (0 - 23)
│    └──────────────────── minute (0 - 59)
└───────────────────────── second (0 - 59, OPTIONAL)

 

node-schedule-tz 라이브러리는 time zone까지 설정할 수 있다. 그냥 node-schedule 라이브러리를 쓰면 서버 시간에 맞춰지는데 다른 국가에 서버가 있는 경우에는 맞지 않을 경우가 있다. 이때는 node-schedule-tz 라이브러리로 타임존까지 맞춰주자. 아래 코드는 서울 시간 기준으로 매일 23시 59분에 실행되도록 스케줄링 한 예다.

 

const schedule = require('node-schedule-tz')

const rule = new schedule.RecurrenceRule();
rule.dayOfWeek = [0, new schedule.Range(0, 6)];
rule.hour = 23;
rule.minute = 59;
rule.tz = 'Asia/Seoul';

module.exports = schedule.scheduleJob(rule, () => {
    console.log('called every 23:59 pm')
})
728x90

'개발 > nodejs' 카테고리의 다른 글

nodejs + postgresql  (0) 2021.01.10
nodejs + multer 파일 업로드  (0) 2021.01.07
nodejs + s3 upload/get  (0) 2021.01.07
node-schedule-tz  (0) 2021.01.07
express  (0) 2020.12.24
debugger  (0) 2020.12.24

express

개발/nodejs 2020. 12. 24. 21:29 Posted by 아는 개발자

nodejs로 서버를 만들 때 유용한 웹 애플리케이션 프레임 워크. 사실상 nodejs의 표준 서버 프레임워크라 봐도 무방하다. 몇몇 함수에 대해서 알아보자.

 

listen 

 

첫번째 인자로 포트 번호를 받고 두번째는 콜백 함수다. 몇번 포트에 서버를 만들 것인지 정하는 함수다.

 

const express = require("express")

const app = express() 

...

app.listen(3000, () => {
    console.log('Server is up on port 3000')
})

 

get, post

 

외부로부터 http GET, POST 요청을 처리 할 수 있다. 콜백함수에서는 요청 인자와 응답 인자를 받으며 응답 인자를 이용해 값을 전달 할 수 있다. 

 

app.get('', (req, res) => {
    res.send('GET request express')
})

app.post('', function (req, res) {
    res.send('POST request to the homepage');
});

 

route 

 

동일한 get, post 주소를 하나로 묶어서 처리하는 것도 가능하다

 

app.route('')
    .get((req, res) => {
        res.send('Hello express')
    })
    .post((req, res) => {
        res.send('POST request to the homepage'); 
    })

 

response method 

 

응답값은 여러가지가 가능하다. 

 

728x90

'개발 > nodejs' 카테고리의 다른 글

nodejs + postgresql  (0) 2021.01.10
nodejs + multer 파일 업로드  (0) 2021.01.07
nodejs + s3 upload/get  (0) 2021.01.07
node-schedule-tz  (0) 2021.01.07
express  (0) 2020.12.24
debugger  (0) 2020.12.24

debugger

개발/nodejs 2020. 12. 24. 20:58 Posted by 아는 개발자

nodejs 도 android studio 나 jetbrain 처럼 강력한 디버깅 툴을 지원한다. 이번 포스트에서는 간단한 사용법을 정리해본다. 

 

1. 코드 내에 중단점 넣기 

 

작업을 중단하고 싶은 특정 위치에 debugger 라는 코드를 끼워 넣는다. nodejs 기본 라이브러리기 때문에 별도의 module 추가는 필요 없다.

 

const fs = require('fs')
const chalk = require('chalk')

const addNote = (title, body) => {
    const notes = loadNotes()
    const duplicateNote = notes.find((note) => note.title === title)

    debugger

    if (!duplicateNote) {
        notes.push({

 

2. 디버깅용 명령어 실행 

 

원래 실행하던 명령어 앞에 inspect를 붙여준다. 디버깅용으로 nodejs가 실행된다. 

 

node inspect app.js add --title="Courses" --body="Note.js"

 

3. chrome://inspect 실행 

 

크롬 브라우저에서 chrome://inspect 주소를 쳐보면 그림 처럼 Remote Target 리스트에 현재 실행하고 있는 node js 프로세스가 보인다. inspect 버튼을 클릭해보자.

 

4. 디버깅 시작 

 

Sources 탭에 들어가면 현재 실행하고 있는 코드가 라인별로 나온다. 오른쪽 상단의 more 버튼을 클릭하면 command 창을 열 수 있고 이거로 각 인자의 값을 볼 수도 있으니 조사식으로 적극적으로 이용하자. 또한 콜스택을 통해서 어떤 순서로 작업이 실행되고 있는지 알 수 있다.

728x90

'개발 > nodejs' 카테고리의 다른 글

nodejs + postgresql  (0) 2021.01.10
nodejs + multer 파일 업로드  (0) 2021.01.07
nodejs + s3 upload/get  (0) 2021.01.07
node-schedule-tz  (0) 2021.01.07
express  (0) 2020.12.24
debugger  (0) 2020.12.24

useContext

개발/react 2020. 12. 22. 21:09 Posted by 아는 개발자

useContext를 사용하면 하위 컴포넌트에서도 상위 컴포넌트에서 전달하는 값을 공유 받을 수 있다. props를 통해서 전달하지 않고 동일한 Context를 넘겨 받은 인자를 통해서 공유가 가능하다. 이 함수를 이용해 state 값과 이를 업데이트 하는 dispatch 함수를 컴포넌트끼리 공유 할 수 있다.

 

1. Context 만들기 

 

하위 컴포넌트에서 공통적으로 공유할 수 있는 React Context를 만든다.

 

import React from 'react';

const NotesContext = React.createContext()

export { NotesContext as default }

 

2. Context 공유하기 

 

공유를 시작하려는 가장 상위 컴포넌트에서 태그를 씌워준다. value 안에 넣어둔 값들은 하위 컴포넌트에서 접근 할 수 있게 된다.

 

const NoteApp = () => {
  
    const [notes, dispatch] = useReducer(notesReducer, [])
  
    ...
  
    return (
      <NotesContext.Provider value={{ notes, dispatch }}>
        <h1>Notes</h1>
        <NoteList />
        <p>Add note</p>
        <AddNoteForm />
      </NotesContext.Provider>

 

3. 공유 데이터 접근 

 

하위 컴포넌트에서는 useContext 함수와 공통적으로 사용하고 있는 Context를 이용해 공유 데이터에 접근 할 수 있다. 아래 코드를 보면 useContext를 이용해 상위에서 공유하는 notes 인자를 접근하고 jsx로 만드는 형태다.

 

import React, { useContext } from 'react';
import Note from './Note';
import NotesContext from '../context/notes-context';

const NoteList = () => {
    const { notes } = useContext(NotesContext)

    return notes.map((note) => (
        <Note key={note.title} note={note} />
      ))
}

export { NoteList as default }
728x90

'개발 > react' 카테고리의 다른 글

useContext  (0) 2020.12.22
useReducer  (0) 2020.12.22
useEffect  (0) 2020.12.21
useState  (0) 2020.12.21
connect  (0) 2020.12.20
react router  (0) 2020.12.17

useReducer

개발/react 2020. 12. 22. 20:59 Posted by 아는 개발자

useReducer는 useState랑 비슷하나 state 업데이트 작업을 담당하는 reducer를 직접 넣어 줄 수 있다는 점이 다르다. 아래 코드에서 주석으로 처리된 useState 코드는 두번째 인자로 state 값을 업데이트 하는 작업을 일괄 담당했는데, useReducer를 사용하면 커스텀 reducer를 추가할 수 있어 action의 타입에 따라서 다른 행동을 취하도록 할 수 있다. 

 

useReducer도 useState처럼 리턴 값은 배열이며 첫번째 인자는 state 값이고 두번째 인자는 reducer에 액션을 보낼 수 있는 dispatch 함수다. 값을 업데이트 할 때는 dispatch 함수에 action 을 설정해서 업데이트 한다.

 

import React, { useEffect, useReducer } from 'react';
import notesReducer from '../reducers/notes';

const NoteApp = () => {
  
    // const [notes, setNotes] = useState([])
    const [notes, dispatch] = useReducer(notesReducer, [])
    
    
// 

const notesReducer = (state, action) => {
    switch (action.type) {
      case 'POPULATE_NOTES':
        return action.notes
      case 'ADD_NOTE':
        return [
          ...state,
          {
            title: action.title, body: action.body
          }
        ]
      case 'REMOVE_NOTE':
        return state.filter((note) => note.title !== action.title)
      default: 
        return state
    }
}

export { notesReducer as default }

 

아래는 Note 를 추가하고 제거하는 작업의 코드다. useState를 사용할 때는 setNote를 이용해서 바로 업데이트 하는 코드를 넣었다면 useReducer에 넣어둔 Reducer를 이용해 action 과 인자를 전달해서 작업을 넘길 수 있다. 액션을 추가하는 코드는 useReducer에서 받아온 dispatch 함수를 사용한다.

 

const addNote = (e) => {
  e.preventDefault()

  // setNotes([
  //   ...notes, 
  //    { title, body }
  // ])
  dispatch({
    type: 'ADD_NOTE',
    title,
    body
  })
}

const removeNote = (title) => {
  dispatch({
    type: 'REMOVE_NOTE',
    title
  })
  // setNotes(notes.filter((note) => note.title !== title))
}

 

useState는 간단한 상태값을 담당할 때 사용한다면 useReducer는 복잡한 상태 값, 중복되는 코드가 많이 생겨 일괄적으로 관리가 필요할 때 사용하면 좋을 것 같다.

728x90

'개발 > react' 카테고리의 다른 글

useContext  (0) 2020.12.22
useReducer  (0) 2020.12.22
useEffect  (0) 2020.12.21
useState  (0) 2020.12.21
connect  (0) 2020.12.20
react router  (0) 2020.12.17

useEffect

개발/react 2020. 12. 21. 20:27 Posted by 아는 개발자

리액트의 useEffect 라이브러리는 컴포넌트나 state에 변화가 생길 때 호출되는 함수다. 두개의 인자를 받는데 첫째 인자는 변경시 호출할 콜백함수고 두번째 인자는 상태를 변경을 감지할 state를 설정한다.  state를 별도로 설정하지 않으면 componentDidUpdate, componentDidMount랑 동일한 역할을 하게 된다. 

 

const NoteApp = () => {
  
  const [notes, setNotes] = useState([])
  const [title, setTitle] = useState('')
  const [body, setBody] = useState('')

  useEffect(() => {
    console.log('load data')
    const notesData = JSON.parse(localStorage.getItem('notes'))
    if (notesData) {
      setNotes(notesData)
    }
  }, [])

  useEffect(() => {
    console.log('update notes')
    const toJson = JSON.stringify(notes)
    localStorage.setItem('notes', toJson)
  }, [notes])

  useEffect(() => {
    console.log('useEffect called')
  })

 

위와 같이 여러개의 useEffect 함수를 둘 수 있다. 첫번째 useEffect에서는 두번째 인자에 빈 배열을 넣었는데 이러면 최초 한번만 호출되게 된다. componentDidMount 콜백과 기능이 유사하다. 두번째 useEffect 함수에서는 notes 상태 값을 인자로 두었다. notes 상태의 값이 변경될 때마다 함수가 호출된다. 세번째 useEffect 함수는 전달인자를 따로 넣지 않아서 내부에 어떤 state가 바뀌더라도 새롭게 호출된다. 

 

 

728x90

'개발 > react' 카테고리의 다른 글

useContext  (0) 2020.12.22
useReducer  (0) 2020.12.22
useEffect  (0) 2020.12.21
useState  (0) 2020.12.21
connect  (0) 2020.12.20
react router  (0) 2020.12.17

useState

개발/react 2020. 12. 21. 20:14 Posted by 아는 개발자

useState는 react 에서 비교적 최근에 나온 state 관리 라이브러리다. 기존에는 컴포넌트 생성할 때 state로 두고 싶은 변수들을 하나의 오브젝트로 관리했다면 useState 라이브러리를 사용해서 변수 별로 나눠서 선언할 수 있다. 

 

const NoteApp = () => {
  
  const [notes, setNotes] = useState([])
  const [title, setTitle] = useState('')
  const [body, setBody] = useState('')

 

useState 인자로 object를 받는데 이 값은 state의 초기 값이다. 배열, 정수형 인자, 문자열 모두 가능하다. 리턴 값으로는 길이가 2인 배열을 내놓는데 첫번째는 state 변수 값이고 두번째 값은 state 값을 변형 시킬 수 있는 setter 함수다. 

 

 const addNote = (e) => {
    e.preventDefault()
    setNotes([
      ...notes, 
       { title, body }
    ])
    setTitle('')
    setBody('')
  }

  const removeNote = (title) => {
    setNotes(notes.filter((note) => note.title !== title))
  }

 

기존에는 setState를 이용해서 모든 오브젝트를 다시 초기화해야 했다면 useState에서 넘어온 setter 함수를 이용해 내가 업데이트 하고 싶은 state만 명시적인 함수로 호출이 가능하기 때문에 관리가 한결 수월해진 측면이 있다. 한 컴포넌트 내에서 관리할 state 인자가 많아질수록 유용해질 기능인 것 같다.

 

728x90

'개발 > react' 카테고리의 다른 글

useReducer  (0) 2020.12.22
useEffect  (0) 2020.12.21
useState  (0) 2020.12.21
connect  (0) 2020.12.20
react router  (0) 2020.12.17
redux  (0) 2020.12.16

connect

개발/react 2020. 12. 20. 11:08 Posted by 아는 개발자

connect 함수는 리액트 앱의 하위 컴포넌트에서 redux store를 접근하는 것을 가능하게 해주는 역할을 한다. 이 함수를 이용해서 컴포넌트 단에서 redux store에 접근하고 액션을 호출 할 수 있게 된다. 이번 포스트에서는 간단한 예제로 connect 함수를 통해 redux store를 사용하는 방법을 다뤄보려고 한다.

 

0. 준비작업

 

connect 함수 소개를 위해 예제와 텍스트와 숫자를 담당하는 redux를 만들었다.

 

BlogStore.js

import { createStore, combineReducers } from 'redux';

const textReducerState = {
    text: '',
    name: 'textReducer'
};

const textReducer = (state = textReducerState, action) => {
    switch (action.type) {
        case 'SET_TEXT':
            return {
                ...state,
                text: action.text
            };
        default: 
            return state;
    }
}

const numberReducerState = {
    numberState: 30,
    name: 'numberReducer'
};

const numberReducer = (state = numberReducerState, action) => {
    switch (action.type) {
        case 'SET_NUMBER':
            return {
                ...state,
                number: action.number
            };
        default: 
            return state;
    }
};

export const configureStore = () => {
    const store = createStore(
        combineReducers({
            text: textReducer,
            number: numberReducer
        })
    );
    return store;
};

 

BlogActions.js

 

export const setText = (
    text = ''
) => ({
    type: 'SET_TEXT',
    text
});

export const setNumber = (
    number = 0
) => ({
    type: 'SET_NUMBER',
    number
});

 

 

1. Provider 

 

configureStore() 함수를 통해 store를 생성하고 Provider 태그에 store를 속성값으로 넣는다. 이러면 하위에 추가되는 component에서 redux store를 바라볼 수 있는 창구가 만들어진다.

 

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { configureStore } from './BlogStore';
import BlogMain from './BlogMain';

const store = configureStore();


const jsx = (
    <Provider store={store}>
        <BlogMain />
    </Provider>
);


ReactDOM.render(jsx, document.getElementById('app'));

 

2. connect 

 

하위 컴포넌트 단에서는 Provider에서 제공하는 store 함수를 connect 함수를 통해서 받아온다. 함수 형식이든 클래스 형식이든 받는 방식은 동일하다. 

 

 

2.1 클래스형식

 

import React from 'react';
import { connect } from 'react-redux';
import BlogDetail from './BlogDetail';


class BlogMain extends React.Component {
    render() {
        console.log(this.props.text)
        console.log(this.props.number)
        return (
            <div>
                <p>BlogMain component</p>
                <BlogDetail />
            </div>
        )
    };
};
const mapStateToProps = (state) => {
    return {
        text: state.text,
        number: state.number
    }
};
export default connect(mapStateToProps)(BlogMain);

 

클래스 형식 컴포넌트를 export 할 때 connect 함수를 사용하고 첫번째 인자에 mapStateToProps 함수를 넣었는데 redux store에 있는 값을 컴포넌트에 어떻게 넘겨줄지 세팅하는 작업이다.  넘겨 받은 값은 component의 props에 들어가서 호출 할 수 있다. 아래 사진은 render() 함수 안에서 console로 찍은 로그다. textReducer와 numberReducer가 출력되는 것을 볼 수 있다.

 

 

2.2 함수 형식 

 

import React from 'react';
import { connect } from 'react-redux';

const BlogDetail = (props) => (
    <div>
        <p>BlogTextDetail</p>
        <p>{props.text.name}</p>
    </div>
);
const mapStateToProps = (state) => {
    return {
        text: state.text
    };
};

export default connect(mapStateToProps)(BlogDetail)

 

함수 형식도 크게 다르지 않다. 컴포넌트 내에서 호출 할 때 this를 부르지 않아도 된다는 점만 다르다. 위 코드로 호출하면 아래 그림처럼 화면 뷰가 그려진다.

 

 

3. Action 

 

connect로 컴포넌트에 전달 할 때 store만 전달 하는것이 아니라 action을 넣을 수 있는 dispatch 함수까지 전달된다. react 디바이스 툴을 사용해보면 component의 props 안에 dispatch가 들어있는 것을 확인 할 수 있다. 

 

 

실제로도 잘 사용할 수 있을 지 테스트 해보자. 방금 전에 사용한 BlogDetail 컴포넌트를 살짝 수정해서 현재 store에 저장된 text를 출력하고 버튼을 추가하고 클릭하면 text를 BlogDetail로 바뀌도록 했다.

 

import React from 'react';
import { connect } from 'react-redux';
import { setText } from './BlogActions';

const BlogDetail = (props) => (
    <div>
        <p>BlogTextDetail</p>
        <p>current store text value: {props.text.text}</p>
        <button onClick={() => {
            props.dispatch(setText('BlogDetail'))
        }}>Change text to BlogDetail</button>
    </div>
);

const mapStateToProps = (state) => {
    return { text: state.text};
};

export default connect(mapStateToProps)(BlogDetail)

 

함수를 실행하고 버튼을 클릭하니 화면이 아래와 같이 store의 text 값이 BlogDetail로 변경됐다.

 

 

4. 총평 

 

코딩을 처음 하는 분이면 이걸 왜 이렇게까지 해야할지 이해가 안될 수 있을 것 같은데 이전에 mvvm, mvc 패턴을 경험해본 개발자들에게는 redux가 크게 어려울 것 같지 않다. 강의 들을 때는 헷갈렸는데 실제로 코드로 짜보니까 어떤 식으로 구조를 잡아야할 지 느낌이 온다. 물론 자바스크립트 언어 특성상 state 세부 이름을 관리할 때 꽤 귀찮음을 겪을 것 같긴 하다.

728x90

'개발 > react' 카테고리의 다른 글

useEffect  (0) 2020.12.21
useState  (0) 2020.12.21
connect  (0) 2020.12.20
react router  (0) 2020.12.17
redux  (0) 2020.12.16
scss  (0) 2020.12.13

react router

개발/react 2020. 12. 17. 19:52 Posted by 아는 개발자

react 에서는 react-router-dom 라이브러리를 통해  들어 오는 주소 별로 별도의 페이지를 보여줄 수 있는 라우팅 기능을 제공한다. 리액트의 특성에 맞게 이 라이브러리는 어떤 페이지로 진입 했을 때 어떤 페이지를 보여 줄 것인지를 컴포넌트 단위로 뽑을 수 있다.

 

1. 라이브러리 임포트 

 

리액트와 필요한 라이브러리를 임포트 한다.

 

import React from 'react';
import { BrowserRouter, Route, Switch, Link, NavLink } from 'react-router-dom';

 

2. Route

 

const AppRouterExample = () => (
    <BrowserRouter>
        <div>
            <Switch>
                <Route path="/" component={() => (<h2>This is DashboardPage</h2>)} exact={true} />
                <Route path="/create" component={() => (<h2>This is Create Page</h2>)} exact={false} />
                <Route path="/edit/:id" 
                    component={ (props) => (<h2>This is Edit Page {props.match.params.id}</h2>)} 
                        exact={true} />
                <Route path="/help" 
                    component={() => (<h2>This is help page</h2>)} 
                    exact={true} />
                <Route component={() => (<h2>This is not 404 page</h2>)} />
            </Switch>
        </div>
    </BrowserRouter>
);

 

<BrowserRouter> 와 <Switch> 태그 내에 위치한 <Route> 태그로 관리하고 싶은 경로를 설정할 수 있다. 이렇게 두면 애플리케이션이 관리하는 경로를 설정 할 수 있게 된다.

 

2.1 path 

 

라우팅할 경로를 정의하는 값이다. 위 페이지에서는 /create, /edit, /help 페이지를 경로로 뒀다. edit 페이지의 경우에는 수정하려는 데이터의 id를 인자로 받을 수 있고 이 값은 component에 전달된다. component 객체에 props로 전달되며 저장되는 필드는 컴포넌트에 있는 값과 같다.

 

2.2 component 

 

해당 경로로 들어올 경우 어떤 component를 보여줄 것인지를 결정하는 곳이다. 직접 컴포넌트를 만들어서 넣을 수 있으며 이 예제에서는 component 필드 내에서 보여줄 수 있는 값을 넣었다. /edit 경로를 보면 props로 Route로부터 인자를 받아오는데 /edit에서 받아오는 id 정보를 확인 할 수 있다.

 

2.3 exact 

 

exact는 이 경로를 명확하게 볼 것인지 말것인지를 설정한다. 평소 익히 쓰던 path와 다른 개념이라 와닿지 않을 것 같은데 exact값이 false면 앞에 부분만 맞아도 해당 페이지로 렌딩이 된다. 예로 /create 는 exact 값이 false이기 때문에 /create/34 로 접근하든, /create/edit 으로 접근하든 모두 create 페이지로 렌딩해준다.

 

3. NavLink 

 

const AppRouterExample = () => (
    <BrowserRouter>
        <div>
            <div>
                <h1>This is Header</h1>
                <NavLink to="/" activeClassName="is-active" exact={true}>Dashboard</NavLink>
                <NavLink to="/create" activeClassName="is-active">Create Expense</NavLink>
                <NavLink to="/edit" activeClassName="is-active">Edit Expense</NavLink>
                <NavLink to="/help" activeClassName="is-active">Help</NavLink>
            </div>

 

NavLink는 하이퍼링크 기능이고 스타일링을 가능하게 한다. 내부에 있는 필드 값을 바꿔서 더 링크를 더 이쁘게 만들 수 있다.

728x90

'개발 > react' 카테고리의 다른 글

useState  (0) 2020.12.21
connect  (0) 2020.12.20
react router  (0) 2020.12.17
redux  (0) 2020.12.16
scss  (0) 2020.12.13
webpack  (0) 2020.12.13