워블리 로켓

Wobbly Rocket

오늘은 기술 이야기가 아닌 개인 프로젝트 이야기를 해보려 합니다. 이름은 워블리 로켓(Wobbly Rocket). 어떤 앱이 될지는 아래와 같이 정리해보았습니다.

자신의 매매 기록을 분석하여 트레이딩 습관과 패턴을 개선하고, 검증된 전략을 단계적으로 자동화하는 개인 트레이딩 플랫폼

사실 이 블로그는 개발하면서 배운 내용들을 정리하는 기술 블로그로 사용되고 있었는데요. AI 도구들이 발전하면서 기술 블로그를 운영하는게 의미가 있나…? 하는 생각에 한동안 손을 놓고 있다가 지금 하고 있는 프로젝트를 하면서 느끼는 점, 고민되는 부분 등을 편하게 남기는것도 의미가 있을 것 같아 오랜만에 키보드를 붙잡았습니다.

워블리 로켓을 시작하게 된 이유

저는 해외 암호화폐 선물 거래소의 개발자였습니다. 사실 처음에는 캔들 차트가 뭐인지도 몰라서 면접때 쩔쩔매던, 매매에 있어서는 백지 같은 사람이였습니다. 일을 하며 거래소를 만드는 사람으로서 매매를 모른다는 건 이상하다고 느꼈습니다. 그래서 직접 매매를 시작하게 되었습니다.

처음에는 초심자의 행운도 있었고 아무 전략 없이, 공부도 없이, 홀짝 게임처럼 진입했는데도 수익이 나기도 했습니다. 하지만 결국은 반복되는 청산과 손실. 계좌는 꾸준히 줄어들었습니다.

문제는 지식이 아니었습니다

결국 큰 손실을 겪고 저는 열심히 공부를 했습니다. 기술적 분석, 지표, 리스크 관리, 켈리 공식…이론은 점점 쌓여갔습니다. 그런데 이상하게도 손실은 계속되더군요.

그러다 깨달았습니다.

내가 몰라서 잃는 게 아니라
내가 나를 통제하지 못해서 잃는 순간이 훨씬 많다는 것.

  • 뇌동매매(감정 매매, 틸트)
  • 복구 심리
  • 포지션 중독
  • 성급함

지식과 별개로, 습관과 감정이 제 계좌를 무너뜨리고 있었습니다. 하지만 이걸 깨달았음에도 제 계좌는 계속 손실만 쌓여갔습니다. 사실 매매를 해보신 분들은 대부분 공감할 이야기일 겁니다. 배운건 많지만 결국 욕심과 감정 때문에 배운대로 매매 하는게 아니라 본능대로 매매 한다는 것을요.

내가 하고 싶은 것

워블리 로켓은 일단 저의 문제를 풀어보고 싶어서 시작한 프로젝트입니다. 그래서 제가 풀고 싶은 문제를 좀 구체화 해보았습니다.

  • 내가 반복하는 실수를 데이터로 마주하고 싶다.
  • 감정이 아니라 시스템으로 의사결정하고 싶다.
  • “운”이 아니라 “재현 가능한 수익이 나는 시스템”을 만들고 싶다.

그래서 어떻게?

예전에 틱낫한 스님의 '화’라는 책을 감명깊게 본 기억이 있습니다. 이 책에서 화를 내는 습관을 고치는 핵심은 '화’가 날때마다 이를 인지하고 바라보라는 것이였습니다.

우리의 문제도 비슷하다고 생각합니다. 먼저 무엇이 문제인지를 명확하게 바라보고 인식할 수 있어야합니다. 내가 문제가 있다는걸 모르거나, 있더라도 그 문제를 구체적으로 보지 못하면 막연하게 나는 안좋은 습관이 있나부다 정도로 끝나게 됩니다.

그래서 저는 제가 풀고 싶은 매매에서의 문제를 아래의 단계로 풀어보려 합니다.

  1. 나의 매매 데이터를 통해 현재 매매를 분석하고 인사이트를 도출한다.(나의 문제를 인식한다.)
  2. 분석한 매매 정보를 바탕으로 수익이 나는 매매 구조를 만들기 위한 '행동’을 제안한다.
  3. 수익이 나는 구조를 시스템화 한다.

마무리

마지막으로 최근 AI 도구들이 정말 무섭게 발전하고 있습니다. 이에 따라 전통적인 기획, 디자인, 개발의 경계도 많이 흐려지고 있구요. 특히나 혼자서 기획, 설계, 개발, 디자인, 실험까지 모두 해야 하는 개인 프로젝트에서 AI는 이제 선택이 아니라 필수입니다.

그동안 개발자로서 기술에 많은 관심이 있었습니다. 하지만 이제는 AI가 발전하며 기술적인 부분의 많은 것들을 AI가 담당하고 있습니다. 이제는 개발자도 문제해결의 영역을 개발문제가 아닌 현실의 문제로 확장해야하는 시기가 온것 같습니다.

워블리 로켓에서 워블리(Wobbly)는 ‘흔들리는’, ‘기우뚱한’, ‘비틀거리는’ 등의 의미를 가진 단어입니다. 매매를 주제로 다루는 서비스에 다소 어울리지 않을 수도 있다고 생각합니다.

하지만 저는 이렇게 생각해보았습니다. 사람은 누구나 실패하고 실수를 합니다. 그리고 대부분의 커다란 성공에도 부족한 시작의 단계가 있을겁니다. 그래서 저는 이 이름이 좋습니다. 완벽하지 않고 부족하고, 실패도 겪지만 결국은 로켓처럼 성공을 향해 날아가리라는 바램을 담아보았습니다. 이 글을 보는 다른 분들도 모두 이렇게 되길 바라며 글을 마칩니다.

rememberUpdatedState 이해하기

Jetpack Compose를 사용하다 보면 상태(State)와 재구성(Recomposition)에 대해 자주 마주하게 됩니다. rememberUpdatedState는 이러한 상태 관리에서 중요한 역할을 합니다. 공식 문서에 나와 있는 설명을 보면 다음과 같습니다.

reference a value in an effect that shouldn’t restart if the value changes

저는 이 설명이 처음에 잘 이해되지 않았습니다. 그래서 rememberUpdatedState를 이해하기 위해 정리 겸 이 글을 쓰게 되었습니다. 이 글이 다른 사람에게 도움이 되기를 바랍니다.

예시 코드

다음은 rememberUpdatedState를 설명하기 위해 공식 문서에서 사용된 샘플 코드입니다. LandingScreen 컴포저블은 최초 생성 후 일정 시간(SplashWaitTimeMillis)이 지나면 onTimeout 함수를 호출하는 역할을 합니다.

중요한 요구사항은 LandingScreen이 재구성 되더라도 딜레이는 늘어나면 안 된다는 점입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Composable
fun LandingScreen(onTimeout: () -> Unit) {

// This will always refer to the latest onTimeout function that
// LandingScreen was recomposed with
val currentOnTimeout by rememberUpdatedState(onTimeout)

// Create an effect that matches the lifecycle of LandingScreen.
// If LandingScreen recomposes, the delay shouldn't start again.
LaunchedEffect(true) {
delay(SplashWaitTimeMillis)
currentOnTimeout()
}

/* Landing screen content */
}

위 코드에서 rememberUpdatedState의 역할은 무엇일까요? rememberUpdatedState를 사용하지 않고 onTimeoutLaunchedEffect에서 사용하면 어떻게 될까요?

rememberUpdatedState 없이 사용하는 경우

LaunchedEffect는 키가 true 로 설정되어 한번만 실행됩니다. 하지만 LandingScreenonTimeout 이 변경될 때마다 재구성됩니다.

이때 LaunchedEffect는 처음 실행될 때 캡쳐한 onTimeout을 사용하기 때문에, LandingScreenonTimeout이 변경되어 재구성된 후에도 처음 받은 onTimeout을 호출하게 됩니다.

1
2
3
4
5
6
7
8
9
@Composable
fun LandingScreen(onTimeout: () -> Unit) {
LaunchedEffect(true) {
delay(SplashWaitTimeMillis)
onTimeout() // 변경된 onTimeout이 아니라 처음 캡처된 onTimeout이 호출됨
}

/* Landing screen content */
}

LaunchedEffect의 키로 onTimeout을 사용하는 경우

LaunchedEffect의 키로 onTimeout을 사용하면, onTimeout이 변경될 때마다 LaunchedEffect가 재실행됩니다.

이렇게 하면 항상 최신의 onTimeout이 호출되지만, LaunchedEffect가 재실행될 때마다 delay(SplashWaitTimeMillis)도 다시 시작되므로 딜레이 시간이 길어질 수 있습니다.

1
2
3
4
5
6
7
8
9
@Composable
fun LandingScreen(onTimeout: () -> Unit) {
LaunchedEffect(onTimeout) {
delay(SplashWaitTimeMillis)
onTimeout() // 항상 최신 onTimeout이 호출됨
}

/* Landing screen content */
}

rememberUpdatedState의 역할

rememberUpdatedState를 사용하면 LaunchedEffect가 재실행되지 않으면서도 항상 최신의 onTimeout을 참조할 수 있습니다. 이는 LaunchedEffect가 재실행되지 않아 딜레이 시간이 유지되면서도, 최신 상태를 유지할 수 있게 합니다.

이제 아래의 rememberUpdatedState에 대한 공식 문서의 한줄 설명이 이해가 되시나요?

reference a value in an effect that shouldn’t restart if the value changes

혹시 이 글이 다소 이해가 되지 않는다면 람다와 클로저, 그리고 이펙트에 대해 다시 학습해보면 도움이 되리라 생각합니다.

구현 보기

마지막으로 rememberUpdatedState의 구현을 코드로 살펴봅시다.

rememberUpdatedState는 아래와 같이 구현되어 있습니다. remember에서 mutableStateOf(newValue)를 통해 newValue를 값으로 갖는 State를 반환 합니다. 따라서 이렇게 반환된 State는 리컴포지션이 발생하더라도 변경되지 않고 캐싱됩니다.

1
2
3
fun <T> rememberUpdatedState(newValue: T): State<T> = remember {
mutableStateOf(newValue)
}.apply { value = newValue }

그리고 바로 apply { value = newValue }를 통해 상태를 최신 값인 newValue로 업데이트 합니다. 즉 이 구현은 상태 객체 자체는 유지하면서 값만 최신으로 업데이트하는 역할을 합니다. 이를 통해 최신 값을 항상 참조할 수 있습니다.