개발자로서 첫 커리어였던 Wavve Tech Internship을 통해 얻은 것들을 정리해봅니다.

인턴십

사원증
내 인생 첫 사원증! 마지막 날에 노트북 초기화하면서 찍었어요ㅋㅋ

지난 5월 중순 10주간의 Wavve Tech Internship이 끝났습니다. 개발자로서 처음으로 현업을 경험하게 됐던 만큼 설렘을 가지고 시작했고, 끝까지 그 설렘을 잃지 않고 열심히 달렸습니다. 아쉽게도 웨이브에서 더 이어가지는 못하게 되었지만, 인턴십을 겪기 전과 비교해보면 아주 큰 성장을 할 수 있었던 시간이었습니다.

이번 글에서는 인턴십을 통해 얻은 것들을 정리해보려고 합니다.

1. 개발자가 협업하는 방법

취업을 준비하며 개인 프로젝트 위주로만 했던 제게 인턴십에서 경험한 '협업'은 정말 새로웠습니다. Jira와 Confluence, Slack을 제대로 협업에 활용해본건 처음이었기에 조금 헤매기도 했죠.

덕분에 개발자가 협업하는 방법을 진지하게 고민하게 되었습니다. 인턴십 전에는 문서화를 최소화하고 코딩에 온전히 시간을 쏟는 것이 좋다고 생각했는데, 같이 일할 때는 좋은 문서 하나가 불필요한 커뮤니케이션 비용을 비약적으로 줄일 수 있다는걸 깨달았습니다. 또한, 테크스펙 작성이 결국 내 목표와 구체적인 방법을 고민하는 과정이었기에 더 나은 방향을 찾을 수 있었고, 오히려 더 빠르게 개발할 수 있었습니다.

테스트 코드가 가지는 의미도 다시 한번 생각해보게 되었습니다. 테스트 코드는 내가 작성한 코드가 정상적으로 동작하는지를 검증하기 위한 것이 아니라, 동료에게 내 코드의 동작을 설명하는 '문서'라고 생각하고 접근했을 때 더 의미가 있었습니다.

같이 일할 때 가장 중요했던건 팀원과 내가 같은 방향을 바라보고 있다는 것을 끊임없이 확인하고 믿을 수 있어야 한다는 것이었습니다. 애자일 방법론을 가지고 개발하는 조직에서는 끊임없이 목표를 정하고 구현하는 과정을 반복하기 때문에, 팀원들과의 효율적이고 효과적인 의사소통이 필수적이었습니다.

웨이브는 일하는 방식 2.0이라는 이름으로 이제 막 애자일 방법론을 도입하기 시작한 곳이었습니다. 덕분에 100여 명 남짓 되는 거대한 개발 조직이 어떻게 애자일하게 일하는 방법을 그려가는지를 맛보는 특별한 경험을 할 수 있기도 했었습니다.

테크서핑데이
신촌 에피소드에서 진행됐던 테크서핑데이 행사

2. Done is Better than Perfect

웨이브 인턴십이 끝나고 제가 따로 요청드려 받은 피드백은 이런 내용이었습니다.

개발 능력이 뛰어나고 가진 지식을 나누는 것을 잘 하며, 구조를 분석하거나 개선하려는 의지가 강하다. 그러나 업무의 핵심에서 벗어날 때가 있다.

저는 주어진 목표까지 도달하는 과정에서 조금이라도 더 나은 방법이 보인다면 적극적으로 도입하는 '버릇'을 가지고 있습니다. 이런 버릇은 개발자로서 좋은 점이기도 하지만, 나쁜 점이기도 했습니다.

완벽한 코드를 작성하려고 끊임없이 고민하다보면, 결국 목표를 달성하지 못하고 시간만 낭비하게 되는 경우가 많았습니다. 물론 그 과정에서 얻게 된 인사이트는 값졌고, 더 나은 코드를 작성할 수도 있었지만, 같은 시간이 주어졌을 때 결과물의 양적 측면에서 떨어지게 된다는건 분명한 사실이었습니다.

Done is Better than Perfect.

페이스북 COO인 셰릴 샌드버그가 언급한 말이라고 하던데, 이 말이 뜻하는 바가 무엇인지 알게 됐습니다. 우리의 고객인 '회사'에 비즈니스 임팩트를 가져다 줄 수 있는 방법은 우선 동작하는 프로덕트를 만들어 내고, 그 후에 더 나은 코드로 리팩토링 해나가는 것이었습니다.

또한, 아직 생기지 않은 문제를 대비한 코드를 작성하는 것이 항상 좋은것만은 아니란 점을 깨달았습니다. 코드의 유지보수성이나 확장성을 고려해 작성하는 것은 좋은 습관이지만, 그것이 불필요한 과정이 되어버리면 오히려 개발 속도를 늦추는 요인이 될 수 있었습니다.

3. 팀 플레이어

'코드를 왜 이렇게 작성했는지'에 대한 가장 좋은 대답은 '팀원을 위해서'에요.

기술 멘토님께서 해주신 가장 기억에 남는 말입니다. 인턴십 기간동안의 제 경험을 예로 한번 들어보겠습니다.

서버와의 통신을 우리는 vue-query(react-query와 같음)로 핸들링하고 있었는데, 제가 맡은 '현재 시청자 수 표시' 파트를 개발하면서 에러를 어떻게 처리할 것인지에 대한 고민이 들었습니다.

이를 해결하기 위해 Suspense를 활용한 QuerySuspense라는 컴포넌트를 설계하고 팀에 제안했습니다. QuerySuspenseexperimental 기능이었으나, 제가 판단하기에 이미 충분히 안정화된 기능이었습니다.

그러나, 여기에는 두가지 문제가 있었습니다.

  1. 목표한 바(현재 시청자 수 표시)와 크게 관련이 없는 기능이다.
  2. 내가 직접 테스트해본 적이 없기 때문에 사용했을 때 어떤 이슈가 발생할 지 알 수 없다.

1번은 앞서 언급한 류의 문제였고, 어떻게 보면 그렇지 않다고 할수도 있는 부분입니다.

그러나 2번은 곰곰히 생각해 보면 심각한 문제였습니다. 제가 제안한 기술을 과연 우리 팀도 충분히 이해하고 있을까요? 그리고, 이 기술을 사용했을 때 발생할 수 있는 이슈를 충분히 예측할 수 있을까요?

이런 의문을 가지고 팀원들과 함께 논의를 거쳤고, 결국 QuerySuspense를 사용하지 않기로 했습니다.

무언가 새로운 기술을 사용한다면, 제가 먼저 테스트를 해보고 도입해도 되겠다는 확신을 가졌을 때 팀에 제시해야 한다는 것을 배울 수 있었던 경험입니다. 그렇지 않다면 혼자 해도 되었을 트러블슈팅을 모두가 하게 될 것이고, 결국 팀 전체에 피해를 가져오고 말게 될겁니다.

방어적 프로그래밍

방어적 프로그래밍(Defensive programming)이라는 용어가 있습니다. (위키피디아)

방어적 프로그래밍은 팀 플레이어로서 중요시해야 하는 가치중 하나입니다. 내가 작성한 코드는 팀원이 이해하기 쉬워야 하며, 예상치 못한 경우에도 예상 가능한 동작을 해야 합니다.

시니어님의 코드를 보며 감탄했던 부분은 바로 이런 요소들이었습니다. 환경 변수를 로드하는 아주 간단한 코드조차도 시니어님은 항상 방어적으로 작성하셨습니다.

const API_KEY = process.env.API_KEY
const API_KEY = process.env.API_KEY
const getApiKey = () => {
  return process.env.API_KEY ?? "기본_API_KEY"
}
const API_KEY = getApiKey()
const getApiKey = () => {
  return process.env.API_KEY ?? "기본_API_KEY"
}
const API_KEY = getApiKey()

위의 두 코드는 같은 동작을 합니다. 그러나, 두번째 코드는 process.env.API_KEYundefined일 경우를 대비해 기본값을 설정해주는 방어적인 코드입니다.

사실 이렇게 하면 코드에 기본 API KEY가 하드코딩되는 문제가 있습니다. 그러나, 사내에서만 사용하는 코드라는 점을 고려하면 이는 큰 문제가 아닙니다. 이렇게 하면 .env를 준비하지 못한 경우에도 코드의 정상 동작을 보장할 수 있다는 장점이 있습니다. 즉, 환경 변수를 로드하는 것 하나 조차 팀원의 실수를 방지하는 코드를 작성하셨던 것입니다.

코드를 무작정 분리하는 것이 능사는 아니다.

관련이 있는 코드는 묶고, 그렇지 않은 코드는 분리하면 코드의 가독성과 유지보수성은 높아집니다. 그러나, 아무 생각 없이 이를 적용하다 보면 오히려 더 복잡하고 이해하기 어려운 코드가 되기 쉽상입니다.

예를 들어, 타입 선언을 코드와 분리하는 것은 신중하게 고려 후 적용해야 합니다. 아래와 같은 코드가 있다고 가정해보겠습니다.

export type StringToIntegerOptions = {
  base?: number
}
 
export const stringToInteger = (target: string, options?: StringToIntegerOptions): number => {
  return parseInt(target, options?.base ?? 10)
}
export type StringToIntegerOptions = {
  base?: number
}
 
export const stringToInteger = (target: string, options?: StringToIntegerOptions): number => {
  return parseInt(target, options?.base ?? 10)
}

아마 옛날의 저였다면 StringToIntegerOptions 타입을 앱의 루트에 위치한 types 디렉터리에 넣으려고 했을겁니다. util-options.ts라는 파일명을 지어주고 '아 나는 코드를 분리했어!' 하며 만족해했겠죠.

그러나, 이렇게 하면 코드의 가독성이 떨어집니다. stringToInteger 함수를 사용하는 개발자는 StringToIntegerOptions 타입을 사용하기 위해 util-options.ts 파일을 찾아봐야 합니다. 이렇게 되면 코드를 이해하는데 더 많은 시간이 소요됩니다. 코드를 분리해 오히려 이해하기 어렵게 만들어버린 것입니다.

항상 '내 코드를 팀원이 읽는다면 어떤 느낌일까?'를 고민하며 개발하는 것이 중요합니다. 이번 인턴십을 거치며 제가 얻은 멋진 인사이트 중 하나입니다.

마치며

콘텐츠웨이브 테크 인턴십은 제게 많은 것을 느끼게 해줬습니다. 팀원들과 함께 일하며 배운 것들이 많았고, 무엇보다도 개발자로서 앞으로 내가 어떤 것에 집중해야 하는지에 대한 방향성을 얻을 수 있었습니다.

주어진 과제를 해결하기 위해 긴 시간 토론하고, 리뷰할 수 있었던 것도 정말 좋았습니다. 물론 야근은 피할 수 없었지만ㅋㅋㅋ 그래도 후회 없는 경험이었습니다😄

제가 개발자가 되기로 한 이유는 세상의 문제를 내 힘으로 풀어낼 수 있는 능력을 가지는 것에 대해 매력을 느꼈기 때문입니다. 다 같이 힘을 합쳐 문제를 해결해야 한다면 제 개인이 아닌 팀의 입장에서 문제를 바라봐야 한다는 점을 절대 잊지 말아야겠습니다.

회사 뷰

웨이브 카페테리아에서 보이는 여의도 빌딩 뷰 멋지더라구요ㅋㅋ 멀리 한강도 살짝 보임