
8년간 Android 개발을 해왔습니다.
Kotlin, Jetpack Compose를 주로 사용했고,
Android 앱 개발에는 익숙했습니다.
그런데 최근 프리랜서 개발자를 준비하면서 고민이 생겼습니다.
"Android만으로 충분할까?"
프리랜서 프로젝트를 검색해봤습니다.
많이 보이는 요구사항:
Android만 할 줄 아는 건 부족했습니다.
프리랜서로 일하려면 다양한 기술이 필요합니다.
특히 웹 개발은:
웹 개발을 배우기로 결심했습니다.
웹 개발을 배우려면 실전 프로젝트가 필요했습니다.
처음엔 이것저것 생각했습니다:
그러다 생각했습니다.
"개발자라면 자기가 만든 이력서 웹사이트가 필요하지 않을까?"
이력서 웹사이트를 만들기로 했습니다.
이유:
웹 기술 스택을 검색해보니... 너무 많았습니다.
React? Vue? Angular?
JavaScript? TypeScript?
CSS? Sass? Tailwind?
Webpack? Vite? Next.js?
머리가 복잡했습니다.
검색하고, 비교하고, 고민한 끝에:
선택:
나중에 알았지만, 이 선택은 정말 잘한 것이었습니다.
"할 수 있을까?"
일단 Next.js 공식 문서부터 읽어보기로 했습니다.
문서를 읽다가 TypeScript 예제 코드가 눈에 들어왔습니다.
interface User {
name: string;
age: number;
}
"잠깐... 이거..."
// Kotlin
data class User(
val name: String,
val age: Int
)
// TypeScript
interface User {
name: string;
age: number;
}
"어? 거의 똑같잖아?"
차이점:
val 없음 (interface는 구조만 정의)Int → numberString → string그게 다였습니다.
"이거... 할 수 있겠는데?"
그날 바로 시작했습니다.
npx create-next-app@latest resume
터미널에 명령어를 입력했습니다.
프로젝트가 생성됐습니다.
npm run dev
localhost:3000을 열었습니다.
"와, 화면이 떴다!"
이력서 데이터 타입을 정의했습니다.
// types/index.ts
export interface ExperienceItem {
id: string;
period: string;
role: string;
description: string[];
tags: string[];
playStoreUrl?: string;
}
Kotlin이었다면:
data class ExperienceItem(
val id: String,
val period: String,
val role: String,
val description: List<String>,
val tags: List<String>,
val playStoreUrl: String? = null
)
"정말 똑같네!"
경력을 자동으로 계산하는 함수를 만들었습니다.
// utils/career.ts
export function calculateTotalCareer<T extends ExperiencePeriod>(
data: T[],
language: 'ko' | 'en' = 'ko'
): string {
const periods = data.map((item) => {
const [startStr, endStr] = item.period.split('~');
return {
start: parseDate(startStr),
end: endStr === "현재" ? new Date() : parseDate(endStr)
};
});
// 겹치는 기간 병합
periods.sort((a, b) => a.start.getTime() - b.start.getTime());
// 총 개월 수 계산
let totalMonths = 0;
for (const period of mergedPeriods) {
totalMonths += calculateMonthsBetween(period.start, period.end);
}
return formatDuration(totalMonths, language);
}
Kotlin이었다면 똑같이 작성했을 코드입니다.
map() → 동일filter() → 동일sort() → 동일<T extends ...> → Kotlin의 <T : ...>와 동일"Kotlin 할 줄 알면 TypeScript도 할 수 있구나!"
경력 섹션 컴포넌트를 만들었습니다.
// components/sections/ExperienceSection.tsx
export function ExperienceSection({ data }: ExperienceSectionProps) {
const { language } = useLanguage();
const totalCareer = useMemo(
() => calculateTotalCareer(data, language),
[data, language]
);
return (
<section>
<h2>Experience</h2>
<p>총 경력: {totalCareer}</p>
{data.map((item) => (
<div key={item.id}>
<h3>{item.role}</h3>
<p>{item.period}</p>
</div>
))}
</section>
);
}
Jetpack Compose였다면:
@Composable
fun ExperienceSection(data: List<ExperienceItem>) {
val language by viewModel.language.collectAsState()
val totalCareer = remember(data, language) {
calculateTotalCareer(data, language)
}
Column {
Text("Experience", style = MaterialTheme.typography.h2)
Text("총 경력: $totalCareer")
data.forEach { item ->
Column(key = item.id) {
Text(item.role, style = MaterialTheme.typography.h3)
Text(item.period)
}
}
}
}
"어? Compose랑 거의 똑같은데?"
useState → remember { mutableStateOf() }useMemo → remember(deps) { }map() → forEach"Compose 할 줄 알면 React도 할 수 있구나!"
그렇게 만들어진 이력서 웹사이트:
🔗 https://glossybigbro.github.io
| 데스크톱 - 라이트 모드 | 데스크톱 - 다크 모드 |
|---|---|
![]() | ![]() |
| 모바일 - 라이트 모드 | 모바일 - 다크 모드 |
|---|---|
![]() | ![]() |
타입 시스템:
함수형 프로그래밍:
컴포넌트:
상태 관리:
8년간의 Android 개발 경험이 웹 개발에 큰 도움이 됐습니다.
새로운 걸 배우는 게 아니라, 이미 아는 걸 다른 방식으로 쓰는 것이었습니다.