15 minute read

나만의 문제 해결과 성장, 두 마리 토끼를 잡기 위해 사이드 프로젝트하기

생각의 변화

어떤 프로젝트를 해야 하는지에 대한 나만의 명확한 기준이 생겼다.

변화 1. 해결책 중심 사고 ➡️ 문제 중심 사고

한동안 하고 싶은 게 너무 많은데 뭘 해야 할지 몰랐다.

뭘 할지 결정하는데 많은 시간을 써도 좋은 답은 나오지 않았다.

각종 아이디어와 해결책으로 머리가 터지기 직전,

단 하나의 생각이 떠올라서 머리가 맑아졌다

“그래서 뭘 해결하려고 하는 거지..?”

“…(아무 생각 없음)”

해결책 중심 사고

신기술, 새로운 언어, 신박한 아이디어로 머리가 가득 차지만, 그걸 가지고 할 수 있는 건 생각보다 많이 없다.

일단 해결책을 만들고 적용할 문제를 찾기는 정말정말 힘들다.

수요 없는 공급은 피해야 한다.

해결책 vs 문제

문제 중심 사고

문제를 찾으면 해결책을 찾기는 쉽다. 그리고 즐겁다.

다만, 문제를 떠올리기 정말 힘들다는 것이다. 진짜 아무 생각도 떠오르지 않아 막막하다

대신, 머리가 쓸모없는 생각들로 가득 차는 것을 막아준다.

그래서 정말 중요한 것에 생각을 집중할 수 있다.

문제 vs 해결책

변화 2. 남의 문제 해결하기 ➡️ 나의 문제 해결하기

문제를 찾았는데도, 해결하려는 의지가 생기지 않았다.

해결책을 만들어도 내가 쓰고 싶은 생각이 들지 않았기 때문이다.

남의 문제 해결하기

이걸 해결하면 남들이 좋아하겠지..? 사람들이 많이 쓰겠지..?

이런 생각만 가지고 고된 개발 과정을 버틸 수 없다.

남의 문제

나의 문제 해결하기

나의 문제는 그렇게 대단하지 않다.

나의 해결책이 기후 위기나 무역 수지 적자 같은 거대한 문제를 해결하지도 못한다.

하지만, 해결하는 과정이 즐겁고 해결하면 내가 행복해진다.

여기에서 끝까지 해낼 힘이 나온다.

나의 문제

변화 3. 나의 해결책 ➡️ 모두의 해결책

신기한 건, 나와 같은 문제를 겪고 있는 사람이 있다는 것이다.

나의 문제를 해결하는 데 집중했을 뿐인데, 남의 문제도 해결되는 상황이 생기기도 한다.

뇌 부위 중, 나를 생각하는 데 쓰이면서 동시에 남을 생각하는데도 쓰이는 전전두피질 때문일지도 모른다.

모두의 문제 해결

나의 사이드 프로젝트 가이드라인

나와 관련 있는 문제를 즐겁게 해결한다.

(이 이야기를 EO에서 몇 번 들은 거 같은데, 직접 깨닫는데 너무 오래 걸렸다.)

 

문제의 발단

지라 티켓을 만들기 너무 귀찮았다.

지라 티켓 생성 힘들

티켓 하나 만드는데 입력할 게 왜케 많은지..

지라 접속부터 귀찮고

실수로 프로젝트 잘못 선택하면 티켓 옮기느라 열불남

가설

내가 쓰는 할일 관리 앱은 Cmd + Shift + A를 누르면 바로 할일을 추가할 수 있다.

ticktick

지라 티켓도 이렇게 쉽게 만들 수 있다면..?

지라 티켓 생성 쉬움

엄청나게 유용하지 않을까?

프로토타입

개발자의 장인정신과 예술가의 혼을 쏟기 전 이게 정말 쓸모 있는지 확인하고 싶었다.

지라 이슈를 생성하는 함수 하나를 구현하고

HTML input, select 만 써서 허술 끝판왕 프로토타입을 만들어봤다.

대충 구현

(암튼 완성)

image

실험결과

프로토타입을 일주일 정도 써봤다.

지라 티켓 하나 만들 때마다 1~2분씩 걸렸던 게 5초로 줄어드니까 너무 편했다.

티켓 만들려고 지라에 들어가는 일이 거의 없어질 정도

(현재는 지라 티켓 생성 화면을 한 달째 써본 적이 없다.)

유용함은 확인했으니 제대로 만들어보기로 결심했다.

전략

창조와 개선을 분리하자. 크리에이터와 에디터가 동시에 될 수 없다.

케빈 캘리 옹이 하신 말씀이다.

지금까지 사이드 프로젝트를 하면서 코딩하면서 기획, 디자인까지 동시에 하다가 실패한 적이 많았다.

개발 과정도 느리고 중간에 멈춰서 결정을 내리는 모든 순간이 고통스러웠다.

그래서 처음에 고통스러워도, 기획 단계부터 확실히 하기로 다짐했다.

계획을 바탕으로 딴 길로 빠지지 않고 앞으로 직진하고 싶었기 때문이다.

코딩을 해야 한다는 강박을 버리기

개발자의 자아가 강해서 항상 사물의 작동원리와 구현방식을 고민한다.

이런 사고방식이 개발자의 역할을 수행할 때는 도움이 된다.

하지만, 기획과 디자인을 할 때는 방해가 되는 것 같다.

사용자를 위해 최선의 UX를 구성해야 하지만, 개발자의 자아가 계속 딴지를 건다.

“이러면 구현하기 복잡할 텐데…”

“굳이 화면을 여러 개를 만들 필요가 있을까? 화면 하나만 만들면 코드도 적을 텐데…”

이런 생각이 자유로운 상상을 막는 것 같아 잠시 코딩을 해야 한다는 생각을 내려놓았다.

반의반 평생 끊임없이 해온 코드 생각을 관둬야 한다니.. 정말 어색했다.

전술

회사에서 배운 기획 - 디자인 - 마크업 - 개발 프로세스를 적용해봤다.

기획

토스의 UX 원칙 하나

One Thing per one Page

하나의 화면은 하나의 메시지만 표현한다. 한 화면에 너무 많은 정보를 전달하지 않도록 지우고, 제외하고, 제거한다.

앱 사용을 위해 2가지 입력을 받아야 한다. (① 지라 토큰 ② 사용할 프로젝트 키 목록)

이렇게 한 페이지에 모든 입력을 받을 수 있다.

CleanShot 2023-02-19 at 17 34 21@2x

이러면 구현은 쉽다.

하지만, 개발자가 아니고선 이런 수수께끼 같은 입력을 완료하긴 힘들 것이다.

여러가지 내용을 텍스트로 입력하는 것은 잘못된 입력을 하지 않을까 하는 불안감도 준다.

한 번에 하나의 입력을 받는 Flow

토스의 UX 원칙에 따라 사용자와 한마디씩 대화를 주고받는 듯한 설정 경험을 디자인했다.

IMG_1609

여기엔 아무런 입력을 받지 않고, 그저 잘하고 있다고 말하는 화면도 있다.

개발자의 마인드라면 절대 넣지 않았을 화면이다. (코드 낭비 아닌가..?)

하지만, 사용자에게 편안한 화면을 상상하니 자연스럽게 추가됐다.

디자인

종이와 펜으로 그린 Flow를 바탕으로 디자인을 완성했다.

화면 모아보기

피그마도 FE 프레임워크처럼 컴포넌트 기능이 있는데, 중복 요소를 줄여줘서 좋다.

마크업

컴포넌트 구조 그리기

디자인을 바탕으로 어떤 컴포넌트를 구현해야 하는지 쭉 적었다.

IMG_1610

드라이 컴포넌트 만들기

컴포넌트 파일(svelte)을 생성하고 아래처럼 가짜로 구현을 했다.

<div>
 <slot/>
</div>

컴포넌트 구조 잡기

페이지 파일을 생성하고 컴포넌트를 불러와서 구조를 잡았다. (구현 X)

<Box>
  <Title>설정을 시작합니다.</Title>
  <MainButton>시작하기</MainButton>
</Box>

컴포넌트 하나씩 구현하기

Tailwind를 사용해서 컴포넌트와 페이지를 하나씩 완성했다.

지라 cli 마크업 작업 중

개발

컴포넌트 구현 -> 각 화면 구현 -> 화면 연결

빠르게 테스트가 가능한 컴포넌트부터, 테스트에 시간이 오래걸리는 화면 이동 작업 순으로 작업했다.

컴포넌트 구현

텍스트 필드, 메뉴, 셀렉트를 스타일부터 기능까지 직접 구현했다.

컴포넌트 라이브러리를 사용해서 시간을 절약할 수 있었지만,

문제에 딱 알맞은 디자인을 해놨는데 라이브러리가 제공하는 디자인에 끼워 맞춰야 하는게 맘에 안 들었다.

각 화면별 구현

API 구현: Result 타입과 목업으로 안전하게 코딩하기

사용자가 입력한 값을 검증하고 처리하기 위해 지라 API와 통신을 해야 한다.

그런데 API 호출은 언제든 실패할 수 있고

페이지가 많은 앱 특성상, 오류가 발생했을 때 적절한 경로를 제공하지 않으면 사용자는 화면에 갇히게 된다.

(얼마나 짜증날까)

모든 에러 상황에 대처하기 위해 Result 타입을 사용해서 API를 모델링했다.

Result 타입 정의

Using Results in TypeScript를 참고했다.

계산의 성공이나 실패를 모델링한다.

type Result<T, E = any> =
  | { ok: true; value: T }
  | { ok: false; error: E }

API 정의

API 타입을 적을 때 반환 타입과 에러 타입을 모두 적는다.

const createIssue = async (data): Promise<Result<CreatedIssue, JiraError>> => {}

API 사용

반환된 result의 성공/실패에 따라 분기 처리한다.

const result = await createIssue({...})

if (result.ok === true) {
  data = result.value
} else {
  errorMessage = result.error
}

Result 타입의 장점

try-catch를 사용하지 않아도 에러를 처리할 수 있고 에러 타입까지 바로 알 수 있다.

사용자에게 실패 가능성을 알려서 항상 에러를 처리하도록 강제하는 것은 덤이다.

목업하기

API가 실패할 수 있다는 것을 아는 것만으론 충분하지 않다.

실제로 API를 실패시켜서 어떻게 앱이 작동하는지 확인해야 한다.

이를 쉽게 하기 위해 간단한 목업함수를 사용했다.

목업함수 구현

(타입은 많이 생략했다)

API 함수를 받아서 같은 인터페이스를 가진 함수를 반환한다.

test 옵션에 따라 목업을 하거나, 실제 함수를 호출한다.

fail 옵션에 따라 성공/실패값을 반환한다.

delay로 얼마나 로딩할지 결정한다.

const mockup = (
  fn: T,
  {
    test = false,
    fail = false,
    delay = 0,
    onSuccess,
    onFail,
  }
) => {
  return async (...args: Parameters<T>): Promise<Result<R, E>> => {
    if (!test) return fn(...args)

    if (delay) await wait(delay)

    if (fail) return Err(onFail(...args))

    return Ok(onSuccess(...args))
  }
}

목업함수 사용

  1. 성공/실패값을 적는다. Result 타입 덕분에 반자동으로 값을 채울 수 있다.

  2. test 옵션을 켜서 목업한다.

  3. API를 실패시키고 싶으면 fail 옵션을 켠다.

const createIssue = mockup(createIssue, {
  test: true,
  fail: false,
  delay: 1000,
  onSuccess() {
    return {
      issueKey: 'BIZFE-9999',
    }
  },
  onFail() {
    return {
      errorMessages: [],
      errors: {
        projectKey: '프로젝트키가 존재하지 않습니다.',
      },
    }
  },
})

API를 목업하면 성공/실패 시 앱이 어떻게 작동하는지 바로바로 테스트할 수 있다.

에러 시 동선이 막히는 부분이 있으면 API 재시도 버튼을 추가하는 식으로 대응할 수 있다.

서버 연결 없이도 앱을 테스트할 수 있다.

화면 연결: 상태 모델링으로 스토어와 라우터를 동시에 표현하기

일반적인 상태 정의

보통은 객체의 모든 값을 nullable하게 정의하고 모든 컴포넌트끼리 상태를 공유한다.

type State = {
  accessToken: string   | null;
  username:    string   | null;
  projectKeys: string[] | null;
  projectKey:  string   | null;
};

근데 이러면 상태가 어떻게 바뀌는지 추적하기 진짜 힘들다.

상태를 읽을 때마다 값이 있을지 없을지 불안에 떨면서 코딩해야 한다.

유효한 상태만 정의하기

상태를 단계별로 나누고 필요한 속성만 정의한다.

예시로 로그인 필요, 로그인 완료, 프로젝트 선택으로 상태를 나눴다.

그리고 Sum 타입으로 상태를 하나로 합쳤다.

type LoginRequired = {
  type: 'LoginRequired';
};

type LoggedIn = {
  type: 'LoggedIn';
  data: {
    accessToken: string;
    username: string;
  };
};

type ProjectSelect = {
  type: 'ProjectSelect';
  data: {
    accessToken: string;
    username: string;
    projects: string[];
  };
};

type State = LoginRequired | LoggedIn | ProjectSelect;

이제 상태는 반드시 이 타입 중에서 하나의 타입만 갖게 된다.

값을 읽을 때마다, 현재 상태가 어떤 값이 있는지 분명히 알 수 있다.

페이지 라우팅

페이지별로 상태를 나누니까 상태의 type 속성을 라우팅할 때 사용할 수 있다.

<script lang="ts">
let state = { type: 'LoginRequired' };

const onAction = ({ detail: newState }) => {
  state = newState
}
</script>

{#if state.type === 'LoginRequired'}
  <LoginRequired on:action={onAction} />
{:else if state.type === 'LoggedIn'}
  <LoggedIn on:action={onAction} />
{:else if state.type === 'ProjectSelect'}
  <ProjectSelect {...state.data} on:action={onAction} />
{/if}

화면 이동

상태 데이터를 그대로 만들어서 action 이벤트로 쏴주면 된다.

그럼 루트 컴포넌트에서 상태를 새로 덮어씌운다.

상태가 바뀌면 그려지는 컴포넌트도 달라지면서 화면이 바뀐다.

<script lang="ts">
  const dispatch = createEventDispatcher<{ action: LoggedIn }>()
  const onClick = () => {
    const {username, accessToken} = await login()

    const action: LoggedIn = {
      type: 'LoggedIn', 
      data: { username, accessToken }
    }

    dispatch('action', action)
  }
</script>

<Button on:click={onClick}>로그인</Button>

상태 저장

항상 모든 상태가 온전하다.

상태를 그대로 저장했다가 불러와서 사용해도 괜찮다.

코드 두 줄만 추가하면 앱을 Resumable 하게 만들 수 있다.

let state = await storage.load() || initialState

const onAction = async ({ detail: newState }) => {
  await storage.save(state)

  state = newState
}

설정 중에 갑자기 앱을 꺼도, 기존 단계를 그대로 진행할 수 있다.

마무리

앱 스크린샷

* 브라우저에서 CMD + J를 누르면 지라 티켓을 만들 수 있다.

* 서버 API를 모두 목업한 상태다.

3줄 정리

  1. 내 문제를 해결하는 사이드 프로젝트를 진행하는 건 즐겁고, 남에게 도움이 되기도 한다.
  2. 제품 구현을 단계별로 진행하면 효율적이다. 대신 기획을 철저히 해야 한다.
  3. 나만의 프로젝트를 하면 미친 척하고 새로운 시도를 해볼 수 있다. Comfort Zone을 벗어나면서 성장한다.