안녕하세요 웹 YB 엄경호입니다.
이번 주차 공유 과제에는 어떤 주제의 글을 쓸까 고민을 하다가, 2022년 AI가 대중적으로 활성화된 이후로 하루도 빠짐 없이 했던 저의 고민에 대해 글을 써볼까 합니다. 이젠 AI가 거의 모든 코딩을 해주고, 심지어 프로그래밍 언어를 몰라도 바이브 코딩을 통해 서비스 또는 게임을 만들어서 돈을 버는 사례가 많아지면서 앞으로 개발자는 어떤 역할을 해야 할까..라는 고민을 참 많이 했습니다.
제가 내린 결론이 궁극적인 정답은 아니겠지만, 많은 시간 고민을 하다 들었던 생각은, 개발자의 가치는 이제 코드 그 자체보다 설계 능력과 추상적 사고에 있다는 것이었습니다.
단순히 "작동하는 코드"를 만드는 것은 AI도 할 수 있지만, 확장 가능하고 유지보수가 용이한 아키텍처를 설계하는 것은 여전히 인간 개발자의 몫입니다.
이번 글은 클린 코드의 원칙을 중심으로 TypeScript와 React 환경에서 어떻게 더 나은 설계와 추상화를 통해 AI 시대에도 필요한 개발자가 될 수 있는지에 대해 작성해보았습니다.
클린 코드?
클린 코드라는 용어를 한번쯤은 들어봤으리라고 생각합니다. 클린 코드(Clean Code)는 로버트 C. 마틴이 2008년에 출판한 책 "Clean Code: A Handbook of Agile Software Craftsmanship"에서 대중화된 용어입니다.
클린 코드란 단순히 "읽기 쉬운 코드"가 아니라, 유지보수와 확장이 용이한 코드를 의미합니다. 로버트 마틴(Uncle Bob)의 말을 빌리자면, "클린 코드는 한 가지 일을 잘하는 코드"입니다.

클린 코드의 핵심 원칙
- 단일 책임 원칙(SRP): 하나의 클래스나 함수는 오직 하나의 책임만 가져야 합니다.
- 개방-폐쇄 원칙(OCP): 코드는 확장에는 열려 있고, 수정에는 닫혀 있어야 합니다.
- 리스코프 치환 원칙(LSP): 하위 타입은 상위 타입을 대체할 수 있어야 합니다.
- 인터페이스 분리 원칙(ISP): 클라이언트는 사용하지 않는 인터페이스에 의존하지 않아야 합니다.
- 의존성 역전 원칙(DIP): 추상화에 의존해야 하며, 구체적인 구현에 의존하지 말아야 합니다.
위 원칙들의 앞글자를 따서 일명 SOLID 원칙이라 부르며, 전공 수업에서도 배우는 내용입니다. SOLID 원칙은 클린 코드의 핵심 기둥으로, 더 유지보수하기 쉽고, 확장 가능하며, 이해하기 쉬운 소프트웨어를 만드는 데 많은 도움을 줍니다.
AI가 코드를 작성하더라도, 이러한 원칙을 이해하고 적용하는 것은 여전히 개발자의 역할입니다. AI는 주어진 요구사항에 맞는 코드를 생성할 수 있지만, 전체 아키텍처의 설계와 코드의 품질을 결정하는 것은 결국 우리 개발자입니다.
// 단일 책임 원칙을 위반한 예
class UserManager {
constructor(private db: Database) {}
async getUserById(id: string): Promise<User> {
return this.db.findUser(id);
}
async validateUserPassword(user: User, password: string): boolean {
// 비밀번호 검증 로직
return hash(password) === user.passwordHash;
}
async renderUserProfile(user: User): string {
// HTML 생성 로직
return `<div>${user.name}</div>`;
}
}
// 단일 책임 원칙을 지킨 예
class UserRepository {
constructor(private db: Database) {}
async findById(id: string): Promise<User> {
return this.db.findUser(id);
}
}
class AuthService {
validatePassword(user: User, password: string): boolean {
return hash(password) === user.passwordHash;
}
}
class UserProfileRenderer {
render(user: User): string {
return `<div>${user.name}</div>`;
}
}
과제를 하면서 또는 지금까지 코딩을 해오면서 한번쯤은 하나의 모듈에 이렇게 많은 기능을 넣어도 되나..?라고 분명 느끼셨으리라 생각됩니다. 위 예시처럼 각 클래스가 하나의 책임만 갖도록 분리하면, 코드의 재사용성과 테스트 용이성이 크게 향상됩니다.
TypeScript를 활용한 타입 설계와 추상화
면접에서 TypeScript에 대한 질문을 받았을 때 TS는 타입을 체크하여 안전성이 더 향상된 언어..라는 뻔한 대답을 했던 기억이 있습니다.
하지만 최근에서야 많이 느끼는 점은 TypeScript는 단순히 타입만 체크하는 것이 아니라 코드 설계에서도 강력한 설계 도구라는 점입니다.
특히 인터페이스와 타입을 통한 추상화는 코드의 가독성과 유지보수성을 크게 향상시킵니다. 그러면 우리는 TypeScript를 어떻게 활용해야 가독성과 유지보수성 향상이라는 결과에 도달할 수 있을까요?
효과적인 전략이라는 제목을 달았지만, 막상 읽어보면 TypeScript를 배우면서 너무나도 많이 접해 이미 무의식적으로 알고 있거나, 아니면 에이~ 누가 저렇게 해라는 생각이 들 정도로 직관적인 내용입니다. 하지만 벌써 6주차에 다와가는 지금 TypeScript에 대해 복습해보는 것도 나쁘지 않을 것 같습니다.
효과적인 타입 설계 전략
1. 의미 있는 타입 정의하기
// 나쁜 예
type UserData = {
n: string;
e: string;
pw: string;
a: number;
};
// 좋은 예
type User = {
name: string;
email: string;
password: string;
age: number;
};
2. 유니언 타입과 교차 타입 활용하기
// 유니언 타입: 여러 타입 중 하나
type ID = string | number;
// 교차 타입: 여러 타입의 조합
type Employee = Person & {
employeeId: string;
department: string;
};
3. 제네릭을 활용한 재사용 가능한 타입 설계
// 제네릭을 활용한 응답 타입
type ApiResponse<T> = {
data: T;
status: number;
message: string;
timestamp: Date;
};
// 사용 예
type UserResponse = ApiResponse<User>;
type ProductResponse = ApiResponse<Product>;
인터페이스와 추상화의 힘
인터페이스는 구현의 세부사항을, 그것을 사용하는 코드로부터 분리합니다. 이는 위에서 언급한 SOLID 원칙 중 "의존성 역전 원칙(DIP)"의 핵심입니다.
// 인터페이스 정의
interface DataStorage {
save(key: string, data: any): Promise<void>;
get(key: string): Promise<any>;
delete(key: string): Promise<void>;
}
// 구체적인 구현 (LocalStorage)
class LocalStorageAdapter implements DataStorage {
async save(key: string, data: any): Promise<void> {
localStorage.setItem(key, JSON.stringify(data));
}
async get(key: string): Promise<any> {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : null;
}
async delete(key: string): Promise<void> {
localStorage.removeItem(key);
}
}
// 구체적인 구현 (IndexedDB)
class IndexedDBAdapter implements DataStorage {
// IndexedDB 구현...
}
// 구체적인 구현 (Firebase)
class FirebaseAdapter implements DataStorage {
// Firebase 구현...
}
// 클라이언트 코드는 구체적인 구현이 아닌 인터페이스에 의존
class UserService {
constructor(private storage: DataStorage) {}
async saveUser(user: User): Promise<void> {
await this.storage.save(`user:${user.id}`, user);
}
}
위 코드에서 UserService는 실제 저장소가 어디인지 몰라도 됩니다.
이는 의존성 역전 원칙의 좋은 예시입니다. 저장소 구현을 변경하더라도 UserService 코드는 수정할 필요가 없습니다.
React 아키텍처와 컴포넌트 설계
React는 컴포넌트 기반 아키텍처로, 클린 코드 원칙을 적용하기 좋은 환경을 제공한다고 생각합니다.
특히 컴포넌트 분리와 상태 관리는 React의 핵심이라 할 수 있습니다.
컴포넌트 설계 원칙
1. 단일 책임 원칙 적용하기
각 컴포넌트는 한 가지 일만 해야 합니다. 따라서 너무 많은 책임을 가진 컴포넌트는 더 작은 컴포넌트 단위로 분리하는 것이 좋습니다.
2. 컴포넌트 합성(Composition) 활용하기
상속보다는 합성을 사용하여 컴포넌트를 재사용하는 것이 더 유용합니다. 특히 React의 children prop은 강력한 도구이므로 이를 잘 활용하면 클린 코드 원칙을 더 잘 지킬 수 있으리라 생각됩니다.
// 카드 컴포넌트
function Card({ children, title }: { children: React.ReactNode; title?: string }) {
return (
<div className="card">
{title && <h2 className="card-title">{title}</h2>}
<div className="card-content">{children}</div>
</div>
);
}
// 합성을 통한 재사용 예시1
function UserCard({ user }: { user: User }) {
return (
<Card title={user.name}>
<p>Email: {user.email}</p>
<p>Role: {user.role}</p>
</Card>
);
}
// 합성을 통한 재사용 예시2
function ProductCard({ product }: { product: Product }) {
return (
<Card title={product.name}>
<p>Price: ${product.price}</p>
<p>In stock: {product.inStock ? "Yes" : "No"}</p>
</Card>
);
}
상태 관리와 데이터 흐름
React 애플리케이션에서 상태 관리는 핵심 설계 고려사항입니다. 특히 상태의 위치와 데이터 흐름 방향은 애플리케이션의 복잡성에 큰 영향을 미치므로 빠짐 없이 이해해서 더욱 철저하게 설계하는 것이 필요합니다.
1. 상태 끌어올리기(Lifting State Up)
리액트에서 데이터는 항상 위에서 아래로 (부모에서 자식으로) 흐르므로 여러 컴포넌트가 동일한 상태를 공유해야 할 경우, 가장 가까운 공통 조상으로 상태를 끌어올리면 됩니다.
2. 상태 관리 추상화하기
복잡한 상태 로직은 커스텀 훅으로 추상화하여 재사용성을 높이고 컴포넌트를 간결하게 유지하는 것이 유지보수와 관리 측면에서 더 좋습니다.
코드 작성을 넘어서는 사고의 확장
앞서 말했듯이 AI가 코드 작성을 담당하더라도, 시스템 설계와 아키텍처 결정은 여전히 개발자의 역할입니다. AI 딸깍 한번으로 수백줄의 코드가 자동으로 나오더라도 우리는 스스로에게 질문하는 것을 멈추면 안됩니다.
- 이 시스템은 어떻게 확장될까?
- 요구사항이 변경된다면 어떤 부분이 영향을 받을까?
- 다른 개발자가 이 코드를 이해하고 수정하기 쉬울까?
등등..
도메인 지식의 중요성
기술적 지식만큼이나 도메인 지식도 중요하다고 생각됩니다. 비즈니스 규칙과 도메인 특성을 이해하면, 우리가 만드는 소프트웨어에 대해 더욱 적합한 추상화와 모델링이 가능해질 것입니다. 그리고 이러한 도메인 지식이 반영된 코드는 단순한 공학적 데이터 구조를 넘어 실제 세상의 비즈니스 규칙을 코드에 명시적으로 표현하므로 클린 코드에 더욱 적합한 설계가 가능할 것이라 생각됩니다.
마지막으로.. 코드 리뷰는 진짜 중요하다
아무리 AI가 코드를 작성하더라도 코드를 리뷰하고 개선하는 능력은 여전히 중요합니다. 덧붙여서 팀원들의 코드를 보고 이해하고, 잘한점과 개선점을 찾아서 공유하는 것도 매우 중요하다고 생각합니다.
처음 코드 리뷰를 시작했을 때는 주로 코드가 동작하는가?에만 집중했습니다. 문법이 맞는지, 버그는 없는지, 요구사항을 충족하는지 등을 확인하고, 네이밍 컨벤션이 일관적인지를 주로 확인했었습니다.
그리고 매주 코드 리뷰를 하며 어떻게 해야 더 좋은 코드 리뷰어가 될까 고민을 했었습니다. 저도 많이 부족하기에 무엇이 정답인지는 잘 모르겠지만, 지금까지 설명한 클린 코드와 SOLID 원칙들을 토대로 최근에는 설계에 집중하여 코드 리뷰를 하려고 노력하고 있습니다.
- 적절한 추상화 수준을 유지하는가?
- 확장 가능하고 유지보수가 용이한가?
- 설계 관점에서 해당 코드는 단일 책임 원칙을 따르는지, 각 클래스와 함수가 명확한 하나의 책임만 갖고 있는지를 확인하고,
- 작성된 코드가 확장에 열려 있고 수정에 닫혀 있는지, 새로운 기능을 추가할 때 기존 코드를 수정하지 않고도 가능한지
- 그리고 가독성과 명확성 측면에서 작성된 코드의 변수명과 함수명이 의도를 명확히 전달하는지
등등, 이와 같은 질문들을 스스로에게 물어보고 체크리스트를 만들어 코드 리뷰를 진행하려고 노력하였습니다.
AI가 단순 코딩을 대체하는 시대에 개발자의 역할은 더 높은 추상화 레벨로 옮겨가고 있다고 생각됩니다. 우리는 코드 작성자에서 이젠 시스템 설계자로, 단순 구현자에서 문제 해결자로 진화해야 합니다.
함께 성장하는 SOPT의 일원으로서 우리 모두 AI한테 밀리지 않는, 더욱 필수적인 개발자로 성장했으면 좋겠습니다. 읽어주셔서 감사합니다~...! 또한 기회가 된다면 "Clean Code (클린 코드) - 로버트 C. 마틴 저" 꼭 한번씩 읽어보시면 큰 도움이 될 것 같습니다 !
(근데 클린 코드, 객체 지향, SOLID 등등 언급하며 이 글을 쓴 저는 해당 전공 시험 반타작을 했습니다)
'4주차' 카테고리의 다른 글
| Tanstack Query (0) | 2025.05.13 |
|---|---|
| Lazy loading과 Suspense (0) | 2025.05.13 |
| useEffect vs useLayoutEffect (0) | 2025.05.13 |
| 변성 가족 알아보기 (공변성, 반공변성, 이변성, 불변성) (0) | 2025.05.13 |
| 🛠️ React Suspense로 비동기 처리와 UX 개선하기 (0) | 2025.05.13 |