신입 개발자 1년, 100번 망한 순간들 (그리고 살아남은 방법)
들어가며: 망하지 않은 신입은 없다
입사 첫날, 회사 노트북을 받아들고 설레는 마음으로 자리에 앉았던 그날을 기억합니다.
"나는 철저히 준비했으니까 실수 안 할 거야."
그로부터 정확히 3시간 후, 저는 팀장님 앞에서 "죄송합니다"라고 말하고 있었습니다. 개발 서버를 날려버렸거든요.
그게 시작이었습니다. 그 후 1년 동안, 저는 상상할 수 있는 거의 모든 실수를 다 해봤습니다. 코드를 날려먹고, 데이터베이스를 망가뜨리고, 프로덕션 서버를 다운시키고, 고객 데이터를 잘못 수정하고...
하지만 여기 제가 있습니다. 살아남았고, 이제는 시니어 개발자가 되었습니다.
이 글은 제가 신입 시절 1년 동안 겪은 실제 실패 100가지와, 각각의 상황에서 어떻게 살아남았는지에 대한 기록입니다.
당신이 지금 실수를 했다면, 이 글을 읽으세요. 당신만 망하는 게 아닙니다.
1부: 첫 주 - "환영합니다, 지옥으로" (실패 1-20)
첫날의 재앙들
실패 #1: 개발 서버를 날려버렸다
# 내가 친 명령어
rm -rf node_modules/
# 실제로 친 명령어 (경로를 잘못 입력)
rm -rf /
어떻게 살아남았나:
- 즉시 팀장님께 보고 (숨기지 말 것!)
- 다행히 개발 서버라 백업 복구로 해결
- 그날 배운 교훈: 명령어 실행 전 경로 3번 확인
실패 #2: Git에 AWS 키를 푸시했다
// config.js - 절대 이러지 마세요
const AWS_ACCESS_KEY = "AKIAIOSFODNN7EXAMPLE"
const AWS_SECRET_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
30분 후:
- GitHub 봇이 자동 감지해서 AWS에 알림
- AWS 계정 일시 정지
- 보안팀에서 연락 옴
어떻게 살아남았나:
- 즉시 커밋 히스토리에서 제거 (git filter-branch)
- AWS 키 전부 재발급
.env파일 사용법 제대로 배움.gitignore에.env추가
실패 #3: 프로덕션 DB에 접속했다가...
-- 테스트하려던 쿼리
UPDATE users SET email = 'test@test.com' WHERE id = 123;
-- Ctrl+Enter를 너무 빨리 눌러서...
UPDATE users SET email = 'test@test.com';
-- WHERE 절을 안 쳤습니다. 전체 유저 15만명 이메일이 test@test.com으로...
어떻게 살아남았나:
- 심장이 멈췄지만 침착하게 DBA에게 연락
- 30분 전 백업으로 복구
- 그날부터 프로덕션 DB는 읽기 전용 계정만 사용
- UPDATE/DELETE 쿼리는 반드시 BEGIN; ... ROLLBACK; 먼저 테스트
실패 #4-10: 환경 설정 지옥
| 실패 번호 | 무엇을 망쳤나 | 얼마나 걸렸나 | 해결 방법 |
|---|---|---|---|
| #4 | Node 버전이 안 맞아서 빌드 실패 | 2시간 | nvm 사용법 배움 |
| #5 | Docker 이미지를 잘못 받아서 용량 폭발 | 1시간 | docker system prune 배움 |
| #6 | 환경변수를 못 찾아서 앱 실행 안됨 | 3시간 | dotenv 설정 방법 이해 |
| #7 | 포트가 이미 사용중이라는 에러 | 30분 | lsof -i :3000 명령어 배움 |
| #8 | 로컬 DB 비밀번호를 까먹음 | 1시간 | 비밀번호 관리자 사용 시작 |
| #9 | SSL 인증서 만료로 API 호출 실패 | 2시간 | 인증서 갱신 프로세스 이해 |
| #10 | 방화벽 때문에 외부 API 접근 안됨 | 4시간 | 네트워크팀에 요청하는 법 배움 |
코드 리뷰에서의 굴욕
실패 #11: "이게 뭔가요?" - 변수명 참사
// 내가 작성한 코드
const a = data.filter(x => x.b > 5)
const c = a.map(x => x.d)
const e = c.reduce((f, g) => f + g, 0)
// 시니어의 코멘트: "이게 뭐 하는 코드죠?"
// 나: "...음... 데이터를 필터링하고... 매핑하고... 더하는...?"
어떻게 살아남았나:
// 리팩토링 후
const activeUsers = users.filter(user => user.loginCount > 5)
const userScores = activeUsers.map(user => user.score)
const totalScore = userScores.reduce((sum, score) => sum + score, 0)
배운 것:
- 변수명은 의미를 담아야 한다
- 코드는 컴퓨터가 아니라 사람이 읽는다
- "나중에 보면 알겠지"는 거짓말
실패 #12: 주석으로 코드 설명하려다 망함
// 나쁜 예 - 내가 작성한 코드
// 유저를 가져온다
const u = getUser()
// 유저가 있는지 확인한다
if (u) {
// 유저 이름을 출력한다
console.log(u.name)
}
// 시니어: "주석이 왜 필요한가요? 코드만 봐도 아는데요?"
어떻게 살아남았나:
// 좋은 예 - 리팩토링
const user = getUser()
if (user) {
console.log(user.name)
}
// 주석은 "왜"를 설명할 때만
// HACK: IE11 버그 때문에 setTimeout 필요
setTimeout(() => updateUI(), 0)
실패 #13-15: 코드 리뷰 받는 자세
| 실패 | 상황 | 문제점 | 올바른 대응 |
|---|---|---|---|
| #13 | "이거 왜 이렇게 했어요?" | "그냥요..." | 설계 의도를 설명할 것 |
| #14 | "여기 버그 있는데요?" | "아니요, 제대로 동작해요!" | 일단 확인하고 인정할 것 |
| #15 | 코멘트 30개 달림 | 방어적으로 반박 | 배우는 기회로 받아들일 것 |
실패 #16: 코드 리뷰 없이 머지했다가...
나: "작은 수정이라 괜찮겠지? 바로 머지!"
3시간 후...
팀장님: "프로덕션 에러율이 500% 증가했는데요?"
나: "...제가 오늘 올린 코드 때문인 것 같습니다"
어떻게 살아남았나:
- 즉시 revert
- 팀 전체에 사과
- 이후 무조건 코드 리뷰 받고 머지
- CI/CD 파이프라인의 중요성 뼈저리게 느낌
Git 전쟁터
실패 #17: force push로 팀원 코드 날림
# 절대 하면 안 되는 짓
git push origin main --force
# Slack 알림: "main 브랜치에 40개 커밋이 사라졌습니다"
# 팀원들: "???"
# 나: "죄송합니다!!!!!!"
어떻게 살아남았나:
- git reflog로 복구 (다행히 가능했음)
--force대신--force-with-lease사용법 배움- main 브랜치는 protected branch로 설정
실패 #18-20: Git 병합 지옥
# 실패 #18: 잘못된 브랜치에서 작업
# feature/login에서 작업하려 했는데 main에서 작업함
git branch # 확인 안함
git commit -m "Add login feature" # main에 바로 커밋
# 실패 #19: merge conflict 해결 못함
<<<<<<< HEAD
const API_URL = "https://dev.api.com"
=======
const API_URL = "https://prod.api.com"
>>>>>>> main
# 나: "이거 어떻게 하는 거죠...?"
# 실패 #20: rebase 하다가 히스토리 꼬임
git rebase main
# 500개의 conflict
# 나: "이제 어떡하죠...?"
git rebase --abort # 백기 투항
2부: 첫 달 - "나 개발자 맞아?" (실패 21-40)
디버깅 악몽
실패 #21: console.log 지옥
// 내 코드의 95%
console.log('여기1')
console.log('여기2')
console.log('data:', data)
console.log('여기3')
console.log('result:', result)
console.log('여기4')
// ... 50개 더
// 시니어: "디버거 사용법 아세요?"
// 나: "...디버거요?"
어떻게 살아남았나:
- Chrome DevTools 디버거 사용법 배움
- breakpoint 설정하는 법
- watch expressions 활용
- console.log는 마지막 수단
실패 #22: "내 컴퓨터에서는 되는데요?"
나: "로컬에서는 완벽하게 동작합니다!"
팀장님: "그럼 왜 스테이징에서 안 돼요?"
나: "...이상하네요"
원인:
- 로컬: Mac, Node 18.0
- 스테이징: Linux, Node 16.0
- 파일 경로 대소문자 차이
- 환경변수 설정 다름
어떻게 살아남았나:
- Docker로 개발 환경 통일
- 환경별 설정 파일 분리
- "내 컴퓨터에서는 되는데"는 금지어
실패 #23-25: 에러 메시지 무시하기
| 실패 | 에러 메시지 | 내 반응 | 결과 | 올바른 대응 |
|---|---|---|---|---|
| #23 | Warning: Memory leak | "경고니까 괜찮겠지" | 2시간 후 서버 다운 | 경고도 에러처럼 대응 |
| #24 | Deprecated method | "일단 동작하니까 OK" | 다음 버전 업데이트 시 전부 깨짐 | 즉시 수정 |
| #25 | Type mismatch | "any 쓰면 되지" | 런타임 에러 폭탄 | 타입 제대로 지정 |
실패 #26: 에러를 숨기기
// 내가 작성한 코드
try {
await saveUserData(data)
} catch (error) {
// 에러 발생 시 그냥 무시
console.log('에러 났는데 뭐 어때')
}
// 결과: 유저 데이터 저장 안됐는데 성공했다고 표시됨
// 고객센터 문의 폭주
어떻게 살아남았na:
// 올바른 방법
try {
await saveUserData(data)
} catch (error) {
logger.error('Failed to save user data:', error)
// 에러를 상위로 전파하거나 적절히 처리
throw new Error('데이터 저장에 실패했습니다')
}
성능 참사
실패 #27: 무한 루프로 브라우저 멈춤
// 내 코드
let i = 0
while (i < 10) {
console.log(i)
// i++ 를 까먹음
}
// 브라우저: (응답 없음)
// 나: "어? 왜 안 돼지?" (새로고침)
// 브라우저: (다시 응답 없음)
// 나: "...아"
실패 #28: N+1 쿼리 문제
// 100명의 유저 목록을 가져옴
const users = await getUsers() // 1번 쿼리
// 각 유저의 주문 정보를 가져옴
for (const user of users) {
user.orders = await getOrders(user.id) // 100번 쿼리
}
// 총 101번의 DB 쿼리
// 로딩 시간: 30초
// 시니어: "이거 왜 이렇게 느려요?"
어떻게 살아남았나:
// 최적화 버전
const users = await getUsers()
const userIds = users.map(u => u.id)
const orders = await getOrdersByUserIds(userIds) // 2번 쿼리로 끝
const ordersMap = groupBy(orders, 'userId')
users.forEach(user => {
user.orders = ordersMap[user.id] || []
})
// 총 2번의 쿼리
// 로딩 시간: 0.5초
실패 #29-32: 메모리 누수
// 실패 #29: 이벤트 리스너 제거 안함
window.addEventListener('scroll', handleScroll)
// 컴포넌트 언마운트 해도 리스너 살아있음
// 페이지 이동할 때마다 리스너 추가됨
// 결과: 메모리 폭발
// 실패 #30: 타이머 정리 안함
setInterval(() => {
fetchData()
}, 1000)
// 컴포넌트 사라져도 계속 실행
// 100개 컴포넌트 = 100개 타이머 = 서버 부하
// 실패 #31: 대용량 배열 복사
const hugeArray = Array(1000000).fill({data: 'x'.repeat(1000)})
const copy1 = [...hugeArray]
const copy2 = [...hugeArray]
const copy3 = [...hugeArray]
// 브라우저: "메모리 부족"
// 실패 #32: 클로저로 메모리 잡아먹기
function createHandlers() {
const cache = new Map() // 이게 절대 해제 안됨
return {
add: (key, value) => cache.set(key, value),
get: (key) => cache.get(key)
}
}
협업 재앙
실패 #33: 문서화를 안 함
// 3개월 전 내가 작성한 코드
function X(a, b, c) {
return a ? (b || c) : (b && c)
}
// 오늘의 나: "이게 뭐하는 함수지...?"
// 원작자: 나
// 나: "과거의 나가 밉다"
실패 #34: Slack 멘션 폭탄
나: @channel 이거 어떻게 하는 거예요? (오전 10시)
나: @channel 급합니다! (오전 10:05)
나: @channel 아무도 안 계세요? (오전 10:10)
나: @here 도와주세요!! (오전 10:15)
팀장님 DM: "channel 멘션은 정말 급할 때만 써주세요"
어떻게 살아남았나:
@channel: 전체 공지 (월 1회 정도)@here: 현재 온라인인 사람 (주 1회 정도)- 개인 멘션: 특정인에게 질문
- 멘션 없이: 일반 질문
실패 #35: 잘못된 타이밍에 질문
금요일 오후 6시 50분
나: "시니어님, 이거 좀 봐주실 수 있나요? 급해요!"
시니어: (퇴근 직전)
나: (다음주 월요일까지 기다림)
올바른 방법:
- 금요일 오후: 간단한 질문만
- 복잡한 질문: 오전 또는 화~목 오후
- 정말 급하면: "언제 여쭤보면 될까요?"
실패 #36-40: 소통 실패 모음
| 실패 | 상황 | 문제 | 해결 |
|---|---|---|---|
| #36 | 모르는데 "네" 라고 대답 | 잘못 이해하고 2일 작업 | "확인하고 답변드리겠습니다" |
| #37 | 스탠드업에서 "문제 없습니다" | 실제로는 막혀있었음 | 솔직하게 어려운 점 공유 |
| #38 | 요구사항 대충 듣고 개발 | 완전히 다른 기능 만듦 | 요구사항 문서화하고 확인 |
| #39 | 에러 났는데 혼자 3일 고민 | 5분이면 해결될 문제였음 | 30분 막히면 바로 질문 |
| #40 | 기술 용어 모르는 척 | 대화가 안 통함 | "이 용어 정확히 뭘 의미하나요?" |
3부: 3개월차 - "이제 좀 적응했다고?" (실패 41-60)
테스트의 부재
실패 #41: "테스트는 시간 낭비" 라고 생각함
// 테스트 없이 기능 개발
function calculateDiscount(price, userLevel) {
if (userLevel === 'VIP') return price * 0.7
if (userLevel === 'Gold') return price * 0.85
return price
}
// 프로덕션 배포
// 2주 후 발견: VIP 고객이 음수 가격으로 구매함
// 원인: 음수 가격 케이스 처리 안함
// 손실: 500만원
어떻게 살아남았나:
// 테스트 작성
describe('calculateDiscount', () => {
test('VIP는 30% 할인', () => {
expect(calculateDiscount(10000, 'VIP')).toBe(7000)
})
test('Gold는 15% 할인', () => {
expect(calculateDiscount(10000, 'Gold')).toBe(8500)
})
test('일반 회원은 할인 없음', () => {
expect(calculateDiscount(10000, 'Normal')).toBe(10000)
})
test('음수 가격은 에러', () => {
expect(() => calculateDiscount(-100, 'VIP')).toThrow()
})
test('0원은 0원 반환', () => {
expect(calculateDiscount(0, 'VIP')).toBe(0)
})
})
// 수정된 함수
function calculateDiscount(price, userLevel) {
if (price < 0) throw new Error('가격은 0 이상이어야 합니다')
if (price === 0) return 0
if (userLevel === 'VIP') return price * 0.7
if (userLevel === 'Gold') return price * 0.85
return price
}
실패 #42: 테스트 커버리지 100% = 버그 없음?
// 커버리지 100% 달성!
function divide(a, b) {
return a / b
}
test('나누기', () => {
expect(divide(10, 2)).toBe(5)
})
// 프로덕션: divide(10, 0) → Infinity
// 나: "테스트는 통과했는데요...?"
배운 것: 커버리지보다 중요한 건 의미 있는 테스트 케이스
실패 #43-45: 테스트 작성 실패
// 실패 #43: 테스트가 프로덕션 코드보다 복잡
test('복잡한 테스트', async () => {
const mockDB = new MockDatabase()
await mockDB.connect()
const mockUser = await mockDB.createUser({...})
const mockSession = await createMockSession(mockUser)
// ... 50줄 더
// 나: "이거 맞게 한 건가...?"
})
// 실패 #44: 테스트끼리 의존성
test('유저 생성', () => {
globalUser = createUser() // 전역 변수 사용
})
test('유저 조회', () => {
expect(getUser(globalUser.id)).toBeDefined()
// 첫 번째 테스트 없으면 실패
})
// 실패 #45: 랜덤값으로 테스트
test('랜덤 테스트', () => {
const result = Math.random() > 0.5
expect(result).toBe(true) // 50% 확률로 실패
})
보안 사고
실패 #46: SQL Injection 취약점
// 위험한 코드
app.get('/user', (req, res) => {
const userId = req.query.id
const query = `SELECT * FROM users WHERE id = ${userId}`
db.query(query)
})
// 공격자: /user?id=1 OR 1=1
// 결과: 모든 유저 정보 유출
어떻게 살아남았na:
// 안전한 코드
app.get('/user', (req, res) => {
const userId = req.query.id
const query = 'SELECT * FROM users WHERE id = ?'
db.query(query, [userId]) // 파라미터 바인딩
})
실패 #47: XSS 공격 허용
// 위험한 코드
function displayComment(comment) {
document.innerHTML = comment
}
// 공격자 댓글: <script>alert('hacked')</script>
// 결과: 모든 방문자에게 악성 스크립트 실행
실패 #48: 비밀번호를 평문으로 저장
// 절대 하면 안 되는 것
const user = {
email: 'user@example.com',
password: '1234' // 평문 저장
}
db.save(user)
// 올바른 방법
const bcrypt = require('bcrypt')
const hashedPassword = await bcrypt.hash('1234', 10)
const user = {
email: 'user@example.com',
password: hashedPassword
}
실패 #49-52: 보안 기본 실수들
| 실패 | 취약점 | 파급효과 | 해결 |
|---|---|---|---|
| #49 | CORS를 *로 설정 | 모든 도메인에서 API 호출 가능 | 허용 도메인 명시 |
| #50 | 세션 타임아웃 없음 | 한 번 로그인하면 영구 접속 | 30분 타임아웃 설정 |
| #51 | 에러에 스택 트레이스 노출 | 내부 구조 정보 유출 | 프로덕션에서는 일반 메시지만 |
| #52 | 파일 업로드 검증 없음 | 악성 파일 실행 가능 | 파일 타입, 크기 검증 |
데이터 재앙
실패 #53: 트랜잭션 없이 돈 이동
// 위험한 코드
async function transferMoney(from, to, amount) {
await decreaseBalance(from, amount) // A 계좌에서 차감
// 여기서 서버 다운되면?
await increaseBalance(to, amount) // B 계좌로 입금
}
// 결과: A 계좌 돈만 사라짐
어떻게 살아남았na:
// 올바른 방법
async function transferMoney(from, to, amount) {
const transaction = await db.beginTransaction()
try {
await decreaseBalance(from, amount, transaction)
await increaseBalance(to, amount, transaction)
await transaction.commit()
} catch (error) {
await transaction.rollback()
throw error
}
}
실패 #54: 데이터 마이그레이션 실패
// 100만 건의 데이터 업데이트
for (const user of allUsers) { // 100만번 반복
await updateUser(user) // 각각 DB 쿼리
}
// 결과:
// - 20시간 소요
// - 중간에 타임아웃
// - 절반만 업데이트됨
// - 롤백 불가능
올바른 방법:
// 배치 처리
const BATCH_SIZE = 1000
for (let i = 0; i < allUsers.length; i += BATCH_SIZE) {
const batch = allUsers.slice(i, i + BATCH_SIZE)
await db.bulkUpdate(batch) // 1000건씩 한 번에
console.log(`Progress: ${i / allUsers.length * 100}%`)
}
// 1시간 안에 완료
실패 #55-60: 데이터 처리 실수들
| 실패 | 문제 | 영향 | 교훈 |
|---|---|---|---|
| #55 | soft delete 안 하고 hard delete | 데이터 복구 불가 | deleted_at 컬럼 사용 |
| #56 | created_at 없이 데이터 저장 | 언제 만들어졌는지 모름 | 타임스탬프 필수 |
| #57 | 중복 데이터 체크 안함 | 같은 주문 10번 생성 | unique 제약조건 설정 |
| #58 | 페이지네이션 없이 전체 조회 | 100만건 한번에 로드 → 메모리 폭발 | limit/offset 사용 |
| #59 | 인덱스 없는 컬럼으로 검색 | 10초씩 걸리는 쿼리 | WHERE 절 컬럼에 인덱스 |
| #60 | 타임존 처리 안함 | 미국 유저는 날짜가 하루 느림 | UTC 저장, 표시할 때 변환 |
4부: 6개월차 - "혼자 할 수 있어" (실패 61-80)
아키텍처 실수
실패 #61: 모놀리스 코드베이스
// 하나의 파일에 모든 기능
// UserController.js (3000줄)
class UserController {
async login() { /* 200줄 */ }
async register() { /* 150줄 */ }
async updateProfile() { /* 180줄 */ }
async deleteAccount() { /* 120줄 */ }
async sendEmail() { /* 90줄 */ }
async uploadAvatar() { /* 140줄 */ }
// ... 20개 메서드 더
}
// 나: "이거 유지보수가 왜 이렇게 힘들지?"
어떻게 살아남았na:
리팩토링 후:
/users
/controllers
- LoginController.js
- RegisterController.js
- ProfileController.js
/services
- UserService.js
- EmailService.js
/validators
- UserValidator.js
실패 #62: 순환 의존성
// A.js
import { B } from './B.js'
export class A {
useB() { return new B() }
}
// B.js
import { A } from './A.js'
export class B {
useA() { return new A() }
}
// 결과: ReferenceError: Cannot access 'A' before initialization
실패 #63: 전역 상태 남용
// 모든 곳에서 접근 가능한 전역 변수
window.currentUser = null
window.isLoading = false
window.errorMessage = ''
window.userData = {}
// ... 50개 더
// 3개월 후
// 나: "이 변수 어디서 수정하는 거지?"
// 검색 결과: 73개 파일에서 사용중
실패 #64-68: 설계 안티패턴
| 실패 | 패턴 | 문제점 | 해결 |
|---|---|---|---|
| #64 | God Object | 하나의 클래스가 모든 일 처리 | 단일 책임 원칙 적용 |
| #65 | Copy-Paste 프로그래밍 | 같은 코드가 20군데 | 공통 함수로 추출 |
| #66 | Magic Number | 코드에 숫자 하드코딩 | 상수로 정의 |
| #67 | Callback 지옥 | 8단계 중첩 콜백 | async/await 사용 |
| #68 | 불필요한 추상화 | 한 곳에서만 쓰는 추상 클래스 | YAGNI 원칙 적용 |
배포 재앙
실패 #69: 금요일 오후 배포
금요일 오후 5시
나: "작은 수정인데 배포해도 되겠지?"
배포 버튼 클릭
오후 5:15 - 에러 알림 폭주
오후 5:30 - 서비스 다운
오후 6:00 - 전 팀원 호출
오후 8:00 - 핫픽스 배포
오후 10:00 - 서비스 복구
교훈: 절대 금요일 오후에 배포하지 말 것
실패 #70: 롤백 계획 없이 배포
// 데이터베이스 스키마 변경
ALTER TABLE users DROP COLUMN old_email;
// 배포 후
// 앱 에러: "Column 'old_email' not found"
// 나: "롤백하면 되지"
// 문제: 컬럼을 이미 삭제해버림
// 롤백 불가능
올바른 방법:
Phase 1: 새 컬럼 추가
ALTER TABLE users ADD COLUMN email VARCHAR(255);
Phase 2: 데이터 마이그레이션
UPDATE users SET email = old_email;
Phase 3: 코드 배포 (email 컬럼 사용)
Phase 4: 2주 후, old_email 컬럼 삭제
실패 #71-75: 배포 실수 모음
| 실패 | 상황 | 결과 | 예방 |
|---|---|---|---|
| #71 | .env 파일을 배포 안함 | 프로덕션 설정 없어서 앱 안 돼 | 환경변수 체크리스트 |
| #72 | DB 마이그레이션 실행 안함 | 스키마 불일치로 에러 | CI/CD에 마이그레이션 포함 |
| #73 | 의존성 버전 안 맞음 | npm install 실패 | package-lock.json 커밋 |
| #74 | health check 엔드포인트 없음 | 서버 다운되어도 모름 | /health 구현 |
| #75 | 로그 레벨 DEBUG로 배포 | 로그 파일 100GB | 프로덕션은 INFO 레벨 |
성능 최적화 실패
실패 #76: 조기 최적화
// 프로젝트 시작 1일차
나: "이 루프를 웹 워커로 돌리면 성능이 좋겠어!"
// 3일 후
나: "웹 워커 디버깅이 왜 이렇게 힘들지?"
// 1주 후
시니어: "이거 그냥 일반 루프로 해도 충분한데요?"
나: "..."
교훈: "Make it work, make it right, make it fast" - 순서 지킬 것
실패 #77: 캐싱 전략 없음
// 매번 DB에서 조회
app.get('/popular-posts', async (req, res) => {
const posts = await db.query('SELECT * FROM posts ORDER BY views DESC LIMIT 10')
res.json(posts)
})
// 인기 게시물은 거의 안 바뀌는데 매 요청마다 DB 쿼리
// TPS: 100 → DB 부하 심각
어떻게 살아남았na:
// 캐싱 적용
const cache = new NodeCache({ stdTTL: 300 }) // 5분 캐시
app.get('/popular-posts', async (req, res) => {
const cached = cache.get('popular-posts')
if (cached) return res.json(cached)
const posts = await db.query('SELECT * FROM posts ORDER BY views DESC LIMIT 10')
cache.set('popular-posts', posts)
res.json(posts)
})
// DB 부하 95% 감소
실패 #78-80: 성능 이슈들
// 실패 #78: 이미지 최적화 안함
<img src="photo.png" /> // 10MB 원본 이미지
// 로딩 시간: 8초
// 해결: 이미지 리사이징, WebP 변환
// 실패 #79: 번들 사이즈 관리 안함
import _ from 'lodash' // 전체 라이브러리 import
// 번들 크기: 5MB
// 해결: import { debounce } from 'lodash-es'
// 실패 #80: 불필요한 리렌더링
function App() {
const [count, setCount] = useState(0)
const heavyComputation = expensiveFunction() // 매 렌더링마다 실행
return <div>{count}</div>
}
// 해결: useMemo 사용
5부: 1년차 - "이제 좀 알겠다" (실패 81-100)
리팩토링 실수
실패 #81: "코드가 지저분하니까 다 새로 짜자"
나: "레거시 코드가 너무 복잡해요. 처음부터 다시 만들겠습니다!"
팀장님: "얼마나 걸릴 것 같아요?"
나: "2주면 충분합니다!"
2주 후: 30% 완성
4주 후: 60% 완성
6주 후: 80% 완성, 새로운 버그 20개 발견
8주 후: "차라리 기존 코드 수정하는 게 나았을 것 같습니다..."
교훈: 동작하는 코드를 버리지 말 것
실패 #82: 테스트 없이 리팩토링
// 기존 코드 (동작함)
function processData(data) {
// 100줄의 복잡한 로직
}
// 리팩토링 (깔끔해 보임)
function processData(data) {
return clean코드(data)
}
// 배포 후
// 기존 기능: 5개 중 2개 깨짐
// 나: "테스트가 없어서 몰랐습니다..."
실패 #83-85: 리팩토링 함정
| 실패 | 문제 | 결과 | 교훈 |
|---|---|---|---|
| #83 | 동작 원리 이해 없이 수정 | 사이드 이펙트 발생 | 먼저 이해하고 수정 |
| #84 | 한 번에 너무 많이 변경 | 무엇이 문제인지 찾기 어려움 | 작은 단위로 커밋 |
| #85 | 인터페이스 변경 | 20개 파일 전부 수정 필요 | 하위 호환성 유지 |
기술 부채
실패 #86: "나중에 고치지 뭐"
// TODO: 이거 나중에 리팩토링 필요
// FIXME: 임시 코드
// HACK: 일단 이렇게
// XXX: 왜 이게 동작하는지 모르겠음
// 6개월 후
$ git grep "TODO"
// 147 matches
나: "나중은 영원히 안 온다"
실패 #87: 의존성 업데이트 미루기
{
"react": "16.8.0", // 현재: 18.2.0
"express": "4.16.0", // 보안 취약점 5개
"lodash": "4.17.11" // 3년 전 버전
}
// 1년 후 업데이트 시도
// 호환성 문제 발생
// Breaking changes 처리 = 2주 소요
올바른 방법:
# 매달 의존성 체크
npm outdated
# 마이너 버전 업데이트 (안전)
npm update
# 메이저 버전 업데이트 (신중히)
# 하나씩, 테스트하면서
실패 #88-92: 기술 부채 누적
| 실패 | 방치한 것 | 1년 후 | 해결 비용 |
|---|---|---|---|
| #88 | 테스트 커버리지 0% | 리팩토링 불가능 | 2달 |
| #89 | 코드 리뷰 없이 머지 | 스파게티 코드 | 3달 |
| #90 | 문서화 안함 | 아무도 코드 이해 못함 | 1달 |
| #91 | 로그 없음 | 디버깅 불가능 | 2주 |
| #92 | 모니터링 없음 | 장애 발생해도 모름 | 1주 |
시간 관리 실패
실패 #93: 시간 추정 완전 실패
나: "이거 2시간이면 됩니다!"
실제:
- 환경 설정: 1시간
- 예상치 못한 버그: 3시간
- 코드 리뷰 수정: 2시간
- 테스트 작성: 2시간
총: 8시간
교훈: 추정 시간 x 3 = 실제 시간
실패 #94: 멀티태스킹의 환상
동시에 하려던 것:
- PR 리뷰
- 새 기능 개발
- 버그 수정
- 회의 참석
- 문서 작성
결과:
- 모든 것이 중도 반단
- 컨텍스트 스위칭으로 생산성 0
- 하나도 완성 못함
해결: 한 번에 하나씩
실패 #95-98: 생산성 킬러들
| 실패 | 습관 | 손실 시간 | 개선 |
|---|---|---|---|
| #95 | 30분마다 Slack 확인 | 하루 2시간 | 집중 타임 2시간 단위로 |
| #96 | 회의에 노트북 가져가서 코딩 | 회의도 코딩도 반쪽 | 회의는 회의만 집중 |
| #97 | 디버깅하다 웹서핑 | 하루 1시간 | Pomodoro 기법 |
| #98 | 완벽주의 | 출시 3주 지연 | "Done is better than perfect" |
성장 정체
실패 #99: 편한 것만 계속 함
// 1년 내내 같은 패턴
app.get('/api/...', async (req, res) => {
const data = await db.query(...)
res.json(data)
})
// 새로운 것 배우기 꺼림
팀원: "GraphQL 써보는 거 어때요?"
나: "REST API가 편한데요..."
팀원: "TypeScript로 마이그레이션 하면 좋을 것 같아요"
나: "JavaScript로도 충분한데요..."
// 1년 후: 성장 정체
어떻게 탈출했나:
- 한 달에 하나씩 새로운 기술 학습
- 불편한 것에 도전
- 실패해도 배우는 게 있다고 생각
실패 #100: 질문하기를 멈춤
초반 (1-3개월):
나: "이거 왜 이렇게 하는 거예요?"
나: "더 좋은 방법 없나요?"
나: "이해가 안 되는데 설명해주세요"
중반 (6개월):
나: "아는 척 하자"
나: "모른다고 하면 무시당할 것 같아"
나: "혼자 찾아보면 되겠지"
결과:
- 잘못된 방향으로 3주 작업
- 이미 있는 기능을 다시 만듦
- 팀의 컨벤션 무시하고 개발
교훈:
모르는 것은 질문하는 것이 능력이다
결론: 100번 망하고 배운 것들
핵심 교훈 TOP 10
-
실수는 성장의 기회다
- 실수 안 하려고 아무것도 안 하는 것보다
- 실수하고 빠르게 배우는 게 낫다
-
즉시 보고하라
- 숨기면 더 큰 문제가 된다
- 빨리 말할수록 빨리 해결된다
-
테스트는 선택이 아니다
- 테스트 작성 시간 < 버그 수정 시간
- "나중에"는 절대 안 온다
-
코드 리뷰는 학습의 장이다
- 방어하지 말고 배우라
- 피드백은 성장의 연료
-
문서화는 미래의 나를 위한 것
- 6개월 후의 나는 남이다
- 주석은 "왜"를 설명해야 한다
-
금요일 오후는 배포 금지
- 주말을 망치고 싶지 않다면
- 화/수/목 오전이 배포 최적 시간
-
30분 막히면 질문하라
- 3일 혼자 고민 < 5분 질문
- 질문은 부끄러운 게 아니다
-
한 번에 하나씩
- 멀티태스킹은 환상이다
- 집중하면 빠르다
-
보안은 나중에 추가하는 게 아니다
- 처음부터 고려해야 한다
- 뚫린 후에는 늦다
-
불편함을 받아들여라
- 편한 길은 성장이 없다
- 새로운 것에 도전하라
신입 개발자에게
당신이 지금 실수를 했다면, 축하합니다. 배울 기회를 얻었습니다.
저는 100번 망했지만, 그래서 지금의 제가 되었습니다.
실수하지 않는 개발자는 없습니다. 있다면 그건 아무것도 안 하는 개발자입니다.
중요한 건:
- 같은 실수를 반복하지 않는 것
- 실수에서 배우는 것
- 실수를 두려워하지 않는 것
실전 체크리스트: 이것만은 지키자
코드 작성 전
- 요구사항을 정확히 이해했는가?
- 비슷한 기능이 이미 있는지 확인했는가?
- 설계를 간단히 그려봤는가?
코드 작성 중
- 의미 있는 변수명을 사용하는가?
- 함수는 한 가지 일만 하는가?
- 하드코딩하지 않았는가?
- 에러 처리를 했는가?
- 보안 취약점은 없는가?
코드 작성 후
- 테스트를 작성했는가?
- 로컬에서 테스트했는가?
- 코드 리뷰를 요청했는가?
- 문서화가 필요한가?
배포 전
- 스테이징에서 테스트했는가?
- 롤백 계획이 있는가?
- 금요일 오후가 아닌가?
- 팀에 공지했는가?
배포 후
- 모니터링을 확인했는가?
- 에러가 발생하는가?
- 기능이 정상 동작하는가?
마치며
이 글을 쓰면서 과거의 저를 다시 만났습니다. 실수투성이였지만, 포기하지 않았던 그 신입 개발자.
당신도 할 수 있습니다.
망해도 괜찮습니다. 다시 일어나면 됩니다. 100번 망하면 100가지를 배웁니다.
그리고 어느 순간, 당신도 이런 글을 쓰고 있을 겁니다.
"나도 망했었는데, 이렇게 살아남았어."
화이팅!
P.S. 이 글에 나온 실수들, 당신도 할 겁니다. 그리고 괜찮습니다. 💪