![[Next.js][Develop] Next.js 클린 아키텍처 적용기](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FVYUes%2FbtsPa3OfFZ8%2FAAAAAAAAAAAAAAAAAAAAADTwoVMR6OVEWQWncGa007Lsj1DFRaqpY86ohs5PsazH%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1753973999%26allow_ip%3D%26allow_referer%3D%26signature%3Dj0EmE3wyv3o6IP%252BqStxnvw6ucEU%253D)
개요
Next.js로 웹 프로젝트를 시작하면서 구조를 어떻게 설계할지 많은 고민이 있었다. 그동안의 프로젝트들은 빠르게 기능을 만드는 데에 집중했었다. 배우면서 작업한 것도 있었고, React와 Next.js의 기능들을 빠르게 다 훑어보기 위함도 컸다. 그러나 프로젝트를 거듭할 수록, 점차 규모가 커지면서 유지보수가 어려워졌다. 의존성과 흐름을 파악하는 데 시간이 많이 들고, 테스트나 기능 추가에도 부담이 커졌다. 유지보수적으로 좋지 않아서 한 번 쓰고 버릴 그런 사이트가 되었고, 더 이상은 그런 사이트들을 만들고 싶지 않았다.
그래서 이 문제를 해결하기 위해 클린 아키텍처를 도입했고, 이 글에서는 그 구조를 어떻게 구성했고, 실제 코드에서 어떻게 활용하고 있는지를 적어보려고 한다.
(클린 아키텍쳐 설명에 대한 글은 너무 많아서 설명은 생략하겠다.)
구조
구조를 왜 나누려고 했는가?
개발을 시작하기 전, 이전 프로젝트들을 생각했을 때 다음의 문제점들이 생길 수 있음을 파악했다.
- 서로 다른 기능의 코드가 섞이면서 모듈성 저하
- 중복된 API 호출 로직으로 코드량 증가
- API 요청 로직이 흩어져 있어 재사용 어려움
- 테스트 시 실제 서버에 의존해 느리고 불안정함
- Apollo와 Axios 요청 방식이 혼용되어 일관성 부족
이러한 경험을 통해, 관심사 분리와 기능/도메인 중심의 구조화가 반드시 필요함을 체감했다.
구조 설계 시 고민했던 선택지
관심사 분리와 기능/도메인 중심의 구조화가 필요함을 체감했고, 클린 아키텍쳐를 적용해겠다고 생각을 했으나 그게 바로 되는 것은 아니었다. 클린 아키텍쳐는 하나의 추상적인 개념이기 때문에 프로젝트에 따라 그 구조나 형태가 조금씩 바뀌게 된다. 그래서 크게 아래의 두 구조에서 어떻게 클린 아키텍쳐를 녹여낼 지를 고민했다.
- 기능 중심 구조 (Feature-based)
- 계층 중심 구조 (Layer-based)
우리가 최종적으로 선택한 구조
기능 중심 분리는 지양하려고 했다
처음에는 기능 중심 분리보다는 계층 중심으로 정리하고 싶었다. 도메인 중심으로 코드를 구성하면 직관적이긴 하지만, 실무에서는 항상 도메인이 명확하지 않다. 도메인을 명확하게 하는 것이 기획자와 개발자의 일이라고 할 수는 있지만 해야하는 것과 어려운 것은 별개의 영역이다.
예를 들어, 챌린지 도메인과 사용자 도메인이 있다고 하자. 사용자의 챌린지를 보여주는 화면은 user 쪽인가? challenge 쪽인가? 나는 개인적으로 이 기능을 사용자의 챌린지로 보고 user 도메인에 속한다고 생각했지만, 팀원마다 이 판단은 달랐다. 기획이 불명확하거나, 두 도메인이 섞이는 기능이 많을수록 도메인 단위 분리는 모호해질 수 있다.
계층 구조도 쉽지만은 않다
그렇다고 entities, repositories, usecases처럼 계층 구조만으로 정리하면 전체 흐름을 파악하는 데 시간이 오래 걸린다. 예를 들어, 로직을 담고 있는 usecase와 그걸 사용하는 컴포넌트가 서로 동떨어진 디렉토리에 존재하면, 어느 기능에서 어떤 흐름으로 작동하는지 추적하기 어려워진다.
결론적으로는
그래서 팀원들과의 논의 끝에, 이번 프로젝트에서는 기능 단위로 디렉토리를 나누되, 그 내부는 계층적으로 정리하는 혼합형 구조를 사용하기로 했다.
외부에서 보면 `features/user`, `features/auth`처럼 도메인 단위로 보이지만, 각 feature 내부는 `presentation`, `domain`, `data` 계층으로 나뉜다. 또한 `shared/` 아래에 공통 유틸, 예외 처리, provider, 스타일, 공통 repository 등을 분리하여 모든 기능에서 재사용할 수 있도록 정리했다. 이 구조는 아래와 같다. ▼
src/
├── app/ # Next.js entry point
├── features/ # 도메인별 기능 (User, Auth 등)
│ └── user/
│ ├── presentation/ # 도메인 전용 컴포넌트와 hook
│ ├── domain/ # Entity, Interface 등 도메인 로직
│ └── data/
│ └── repositories/ # API 구현체
├── graphql/ # GraphQL Codegen 결과물
├── shared/ # 공통 모듈 집합
│ ├── base/ # BaseApolloRepository, BaseAxiosRepository
│ ├── components/
│ ├── constants/
│ ├── dependency_injectors/
│ ├── exceptions/
│ ├── interfaces/middleware/
│ ├── lib/
│ ├── providers/
│ └── styles/
└── test/ # 테스트 관련 코드
이 구조는 테스트 가능성, 흐름 추적, 유지보수 용이성, 도메인별 분리와 같은 실질적인 고민을 반영한 결과물이다.
세부 설계
의존성 주입과 DI Container 구성
의존성 주입이 필요한 이유?
일반적으로 React에서는 Provider와 Context를 통해 AxiosInstance나 ApolloClient 같은 객체를 컴포넌트에 주입한다. 하지만 클린 아키텍처에서는 이런 방식이 어렵다. 우리가 작성하는 Repository, UseCase는 React나 Next.js에 전혀 의존하지 않기 때문이다. 프레임워크 독립성을 유지하려면 의존 객체를 외부에서 주입해야 한다.
예를 들어 UserRepositoryImpl은 내부에서 ApolloClient를 생성하지 않고, 생성자를 통해 외부에서 주입받는다. 이는 테스트 환경에서도 동일하게 동작한다. 실제 클라이언트 대신 MockClient를 주입하면 네트워크 요청 없이 테스트를 수행할 수 있다.
또한 Axios → Fetch로 바꿔야 할 상황에서도 구현체만 교체하면 되므로, 구조를 변경하지 않아도 된다. 이런 구조는 의존성 역전 원칙을 따르며, 유지보수성과 테스트 효율을 크게 높인다.
tsyringe를 이용한 의존성 주입
우리는 tsyringe를 활용해 의존성 주입을 구성했다. 이를 통해 인터페이스 - 구현체 간의 의존성 결합을 제거하고, 테스트 시 mock 객체로 교체가 가능하도록 했다. ▼
// shared/dependency_injectors/register_user_repository.ts
import { container } from 'tsyringe';
import { IUserRepository } from '@/features/user/domain/repositories/IUserRepository';
import { UserRepositoryImpl } from '@/features/user/data/repositories/UserRepositoryImpl';
import { UserRepositoryMock } from '@/features/user/data/repositories/UserRepositoryMock';
import { DI_TOKENS } from '@/shared/dependency_injectors/di_token';
import { ApolloClient, NormalizedCacheObject } from '@apollo/client';
const USE_MOCK = process.env.NEXT_PUBLIC_USE_MOCK === 'true';
container.registerInstance<IUserRepository>(
DI_TOKENS.UserRepository,
USE_MOCK
? new UserRepositoryMock()
: new UserRepositoryImpl(
container.resolve<ApolloClient<NormalizedCacheObject>>(DI_TOKENS.ApolloClient)
)
);
// shared/dependency_injectors/register_usecases.ts
import { CreateUserUseCase } from '@/features/user/domain/usecases/CreateUserUseCase';
import { GetUserUseCase } from '@/features/user/domain/usecases/GetUserUseCase';
import { container } from 'tsyringe';
container.registerSingleton(CreateUserUseCase);
container.registerSingleton(GetUserUseCase);
테스트에서는 Mock이 꼭 필요
테스트 시 매번 실제 API를 호출하는 건 너무 느리고 불안정했다. 이를 해결하기 위해 Apollo의 MockLink를 사용해 가짜 클라이언트를 구성하고, 쿼리-응답을 직접 정의했다. UserRepositoryMock은 실제 구현과 동일한 인터페이스를 따르기 때문에, 기존 코드에서는 그대로 교체하여 테스트할 수 있다. 그 결과 빠르고 안정적인 테스트 환경을 구축할 수 있었다.
Repository 계층
Repository가 필요한 이유
Repository는 도메인과 외부 데이터 소스(API, DB 등) 사이를 중재하는 계층이다. 도메인 로직이 어떤 방식으로 데이터를 가져오는지는 몰라도 되도록 만드는 것이 목적이다.
이 구조가 필요한 이유는 다음과 같다:
- 데이터 소스와의 결합도 제거: GraphQL, REST, IndexedDB 등 어떤 방식이든 Repository 내부 구현만 교체하면 된다.
- 도메인 순수성 유지: 도메인 계층이 네트워크 라이브러리(Axios, Apollo 등)나 프레임워크에 의존하지 않게 된다.
- 테스트 용이성 확보: 실제 API 대신 Mock 객체를 주입할 수 있어, 독립적인 테스트가 가능하다.
- 관심사 분리: Repository는 데이터 접근, 도메인은 “비즈니스 의미”에 집중한다.
프로젝트에서의 Repository
User 도메인을 기준으로 Repository 계층은 아래처럼 구성되어 있다:
- IUserRepository: 유저 기능에서 사용할 메서드를 정의하는 인터페이스. 도메인 계층은 이를 통해 데이터 접근 방식과 분리된다.
- UserRepositoryImpl: 실제 GraphQL API 호출을 담당하며, 예외 처리와 파싱 로직은 BaseApolloRepository에서 공통적으로 처리한다.
- UserRepositoryMock: 테스트를 위한 가짜 구현체로, 실제 서버 없이도 UseCase를 검증할 수 있게 만든다.
UserRepositoryImpl 내부에는 해당 도메인의 API 호출을 비롯한 로직들이 들어있다. 유저 정보를 id로 조회하는 findById는 다음과 같이 구현되어 있다. ▼
async findById(id: string): Promise<UserModel | null> {
return this.parseData<UserModel>(
GetMemberDocument,
{ id },
(json) => UserModel.create({ id: json.id, email: '', name: '' }),
{ fetchPolicy: 'network-only' }
);
}
parseData는 응답 파싱 및 예외 처리를 담당하는 공통 메서드다. parseData는 API호출, 파싱, 예외처리 및 방어의 기능을 수행한다. 모든 API들을 parseData로 처리하면 네트워킹 로깅과 예외처리 로직이 모든 부분에서 동일해진다.
물론 이 부분이 없어도 문제가 되진 않지만, 이를 통해 각 구현체에서는 오직 "무엇을 가져오고 어떤 형태로 변환할지"에만 집중할 수 있다. 특별한 로직이 없는 CRUD만 하는 거라면 구조를 상당히 단순화 시킬 수 있게 된다.
UseCase 계층
UseCase가 필요한 이유
UseCase는 사용자의 하나의 액션 단위 또는 기능 흐름을 담당하는 계층이다. 단순히 Repository를 호출하는 것이 아니라, 그 과정에서 도메인 객체를 생성하고, 검증하며, 흐름을 제어하는 역할까지 맡는다.
이 계층이 필요한 이유는 다음과 같다:
- 비즈니스 로직을 캡슐화: 복잡한 흐름을 하나의 단위로 묶어, UI나 프레임워크와 분리된 상태에서 독립적으로 동작시킬 수 있다.
- 재사용성: 동일한 로직을 여러 화면, 서비스, 트리거에서 재사용할 수 있다.
- 단위 테스트 가능: 외부 의존성만 주입하면 로직 단위로 쉽게 테스트할 수 있다.
- 표준화된 흐름: 모든 기능이 UseCase.execute()라는 동일한 형태로 실행되기 때문에, 유지보수가 용이하다.
프로젝트에서의 UseCase
예를 들어, 사용자를 생성하는 흐름을 담당하는 CreateUserUseCase는 다음과 같이 구현되어 있다. ▼
export class CreateUserUseCase {
constructor(
@inject('UserRepository')
private readonly userRepository: IUserRepository,
) {}
async execute(request: CreateUserRequest): Promise<CreateUserResponse> {
const { id, email, name } = request;
const user = UserModel.create({ id, email, name }); // 도메인 객체 생성
await this.userRepository.save(user.id); // Repository 호출
return {
id: user.id,
email: user.email,
name: user.name,
};
}
}
이 구조에서는 다음과 같은 특징이 드러난다:
- 불변성
Entity는 반드시 도메인 레이어에서 생성되며, 불변성과 내부 로직을 유지한다. - 흐름 담당
UseCase는 Entity의 생성 → Repository 저장 → 응답 변환까지의 흐름을 담당한다. - 블랙박스 취급
UI는 이 UseCase만 호출하면 되고, 로직 내부 구조에 대해 알 필요가 없다.
요약 정리
계층 | 책임 | 장점 |
Repository | 도메인과 데이터 소스 분리 | 데이터 소스 교체 유연, 테스트 가능, 관심사 분리 |
UseCase | 사용자 단위 로직 실행 흐름 정의 | 비즈니스 캡슐화, UI와 분리, 테스트 가능 |
Repository와 UseCase는 단순한 코드 추상화가 아니라, 클린 아키텍처의 핵심 원칙을 구현하는 수단이다. 각각의 계층은 의존 방향과 관심사를 명확히 분리함으로써, 구조적 안정성과 유지보수 가능성을 동시에 확보하게 된다.
예외 처리 공통화
API 통신에서는 예외가 자주 발생한다. 서버에서 반환하는 에러 메시지가 일정하지 않거나, 네트워크 환경이 불안정할 경우, 혹은 JSON 파싱 구조가 예상과 다를 경우가 많다. 이러한 다양한 예외들을 각 Repository에서 일일이 처리하면 다음과 같은 문제가 발생할 수 있다.
- 예외 처리 로직이 중복되고 일관성이 없음
- 흐름 파악이 어려워지고, 책임이 분산됨
- 테스트할 때 엣지 케이스의 경우 재현하기 어려움
이 문제를 해결하기 위해, 우리는 공통 예외 추상 클래스인 `BaseException`을 만들고, 그 하위에 다양한 HTTP/네트워크/파싱 예외들을 정의했다. `BaseException`은 다음과 같이 생겼다. ▼
// shared/base/BaseException.ts
export abstract class BaseException extends Error {
/** 사용자에게 보여줄 메시지 */
readonly msgForUser: string;
constructor(msgForDev: string, msgForUser: string) {
super(msgForDev);
this.msgForUser = msgForUser;
// 프로토타입 체이닝 유지
Object.setPrototypeOf(this, new.target.prototype);
}
}
이 클래스를 기반으로 모든 예외들은 다음처럼 상속 구조로 정의된다. ▼
// shared/exceptions/ServerExceptions.ts
export class BadRequestException extends BaseException {
constructor(
msgForDev: string = 'Bad request.',
msgForUser: string = '잘못된 요청입니다.'
) {
super(msgForDev, msgForUser);
}
}
export class UnauthorizedException extends BaseException {
constructor(
msgForDev: string = 'Unauthorized access.',
msgForUser: string = '인증이 필요합니다.'
) {
super(msgForDev, msgForUser);
}
}
모든 예외 클래스는 개발자 메시지(`msgForDev`)와 사용자 메시지(`msgForUser`)를 분리하여 전달할 수 있도록 설계했다. 이를 통해 다음을 동시에 만족시킬 수 있다.
- 개발 단계에서는 `console.error(e.message)`를 통해 디버깅
- 사용자 화면에서는 `e.msgForUser`를 이용해 UI 친화적 메시지 출력
`msgForUser`의 경우 사용자에게 보여지는 부분인데, 이 부분은 백엔드의 `message`에서 받아오기도 한다. 사용자에게 서버 상태가 어떤 지 정도는 말 해줄 필요도 의무도 없지만, 대략적인 서버 상태에 대한 고지가 필요할 경우 백엔드에서 `message`를 받은 뒤, `CustomException`으로 던지는 것도 가능하다.
예외 처리는 각 `Repository`에서 직접 하지 않고, 다음과 같이 `BaseApolloRepository` (혹은 `BaseAxiosRepository`) 내부의 `parseData()`에 위임한다. ▼
async findById(id: string): Promise<UserModel | null> {
return this.parseData<UserModel>(
GetMemberDocument,
{ id },
(json) => UserModel.create({ id: json.id, email: '', name: '' }),
{ fetchPolicy: 'network-only' }
);
}
이 방식의 장점은 다음과 같다:
- API 요청/파싱/에러 처리 분리가 명확해짐
- 테스트 시 예외 상황을 쉽게 재현 가능
- 사용자 경험을 고려한 에러 메시지 전달 구조
결과적으로 `BaseException`과 그 하위 예외들은 단순한 에러 클래스를 넘어서, 로직과 사용자 경험을 분리하는 인터페이스 역할을 한다.
장점과 한계
장점
독립적 모듈
백엔드 API 변화는 없이 프로젝트에서 UI에 변화를 줘야 하는 때가 올 때, 페이지에는 UI와 로직이 엄격하게 분리된 상태이므로 Presentation 부분만 도려내어 바꾸는 것이 가능해진다. 특정 페이지를 위한 로직은 페이지가 바뀌면서 사라지거나 변경이 필요하겠지만, 이 역시 UseCase로 관리되고 있기에 다른 페이지나 모듈에 영향을 주지 않는다. 사용하지 않는다면 쉽게 제거할 수 있게 된다. 5S 원칙에도 잘 들어맞는다. (물론 히스토리 파악을 위해 코드를 남겨놓는 것이 좋을 수 있다. 엄격하게 5S 원칙을 지키고 싶다면 별도의 문서를 제작해야 한다.)
테스트 용이
Mock과 Impl은 자유롭게 스위칭이 가능하기 때문에 테스트에서도 독립적이다. 특히 Mock 구조를 활용하면 실패 케이스나 엣지 케이스를 마음대로 구성할 수 있어서 테스트 가능 범위도 높아지고 테스트 속도 역시 빨라진다.
공통된 로직 처리로 일관된 경험 제공
또한 공통 예외 처리 구조는 사용자 경험을 일관되게 만들어준다. 어떤 Repository든지 동일한 방식으로 예외를 던지고 처리할 수 있기 때문에, 에러가 났을 때 사용자에게 보여주는 메시지도 통일되고, 개발자 입장에서도 예외 흐름을 빠르게 추적할 수 있다. 이로 인해 UI 코드에서는 try-catch 블록 하나로 모든 예외를 받아 처리하면 되므로 로직이 간결해진다. 심지어 API 호출부에서는 parseData가 이를 모두 수행해주기 때문에, 어떤 데이터를 어떻게 변환하여 제공할 지만 고민하면 된다.
장기적으로는 협업에 굉장히 유리
그리고 구조적으로는 도메인 단위 폴더가 잘게 나뉘어 있지만, 그 내부는 계층적으로 통일되어 있어서 학습 곡선만 넘어서면 팀원 간 협업이나 파악이 훨씬 쉬워진다. 추후 새로운 도메인이 추가되거나, 인수인계를 할 때도 흐름이 명확하게 유지된다.
한계
러닝 커브가 높다
구조 자체가 처음엔 복잡하게 느껴질 수 있다. 특히 React나 Next.js에서는 해당 개념을 잘 사용하지 않기에, UseCase나 Repository 같은 개념이 다소 낯설 수 있다. Flutter에서는 클린아키텍쳐를 도입하고자 하는 움직임이 꽤 보이고 이를 적용하고자 하는 사람도 상당히 많아 참고할 게 많았다. 프로젝트에 처음 들어왔을 때, 어디서부터 기능이 시작되고 데이터가 흘러가는지를 파악하는 데 시간이 걸릴 수 있다.
코드 작성량이 많아진다
단순한 API 호출 하나를 만들기 위해 interface, impl, usecase, model 등을 만들어야 하기 때문에 지금 당장만 본다면 오히려 느리고 복잡하다고 느낄 수도 있다. 이를 최대한 줄이기 위해 parseData라던가 baseRepository 등등을 만들어둔 것임에도 도메인을 처음 만들 때 `IRepository`, `RepositoryMock`, `RepositoryImpl`을 만들고 repository에 있는 메소드들을 UseCase로 다시 만들어야 하기에 시간이 상당히 오래걸린다.
"어차피 TanStackQuery(구 ReactQuery)를 사용할건데 메소드만 잘 만들어두면 되는거 아냐?" 라고 하면 솔직히 반박하기가 어렵다. 그래도 그건 어디까지나 상태관리의 영역이고, 지금 하는 UseCase들은 비즈니스 로직을 철저히 분리하기 위함이라고 하면 반박은 된다. 하지만 "규모가 작은 프로젝트에서 거기까지 하게?" 라고 하면 조금 과한 면도 있다.
그렇게 생각하면 모든 기능에 UseCase가 필요한지도 고민이 된다. 단순한 데이터 조회나 버튼 클릭 후 상태만 바꾸는 간단한 로직도 UseCase로 추상화할 필요가 있는가에 대해서는 팀마다 기준이 다를 수 있다. 과하면 오히려 불필요한 추상화로 유지보수가 힘들어질 수도 있다.
타입이 너무 많아질 수 있다
또한 타입스크립트를 사용하면서 Interface와 Model, DTO 간의 경계를 명확히 하려다 보면 타입 정의가 많아지고, 타입 변환에 따른 코드가 증가할 수 있다. 초보자나 설계 의도가 명확하지 않은 팀에서는 이 경계가 오히려 혼란을 줄 수 있다. 그래서 현재 프로젝트에서는 DTO가 없다. 물론 GraphQl을 사용하고, Codegen까지 사용하기 때문에 더더욱 필요가 없어진 부분이긴 하다.
양날의 검이 된 Mock
마지막으로, Mock을 도입하면 테스트가 편해지긴 하지만, 실제 API 구조가 변경될 경우 Mock도 함께 수정해줘야 한다. 테스트 코드가 많아질수록 그것을 유지하는 비용도 커지며, 이 부분도 고려해야 할 현실적인 포인트다.
마치며
이번 구조 설계에서 우리가 가장 중요하게 신경 쓴 점은 명확한 도메인 분리, 의존성 역전 원칙의 적용, 그리고 공통 책임의 일관된 관리였다. 기능별로 디렉토리를 나누고 내부를 계층적으로 정리한 덕분에, 새 기능을 추가하거나 기존 도메인을 확장할 때 어디에 어떤 코드를 넣을지 명확했다. 팀원 간 협업이나 코드 리뷰도 훨씬 효율적으로 진행됐다.
솔직히 말해서 클린 아키텍쳐를 적용했다고는 했지만, 제대로 적용했는지는 의문이다. 현실적으로 클린 아키텍쳐의 모든 부분을 넣는 것은 물리적으로 불가능하기에 많은 부분을 덜어내다 보니 처음의 클린 아키텍쳐와는 좀 달라진 느낌이다. 추상적 개념을 프로젝트에 맞게 녹이다 보면 모습이 바뀔 수 있다고 생각은 하지만 너무 많이 바뀌어 이게 맞나...? 싶기도 하다.
그리고 웹에서는 클린 아키텍쳐를 도입하고자 하는 시도가 많지가 않아 보인다. 상태관리 라이브러리도 굉장히 잘 되어있어서 모바일에 비해 클린 아키텍쳐를 도입했을 때 향상되는 비율이 많지가 않은 느낌이기도 하다. 그래서 적은거 같다는 생각이다.
그렇지만서도 흐름을 지켜준다는 면에서 사용하는게 좋다고 생각한다. 대부분의 버그는 기술적으로는 통과했지만 의미론적으로는 잘못된 상태에서 온다. 의미는 어느정도의 흐름을 타기 때문에 (아무래도 맥락없이 해석되는 건 없다) 흐름이 중요한 부분이다. 그 흐름을 지켜주는 것만으로도 디버깅이 편해지고 에러 발생이 줄어든다고 생각이 된다.
결국 완벽한 클린 아키텍처를 구현하는 건 쉽지 않고, 각 프로젝트와 팀 상황에 맞춰 유연하게 적용하는 것이 더 현실적이라는 생각이 든다. 중요한 것은 원칙을 무조건 지키는 것이 아니라, 내 프로젝트에서 무엇이 정말 필요한지 고민하고, 의미 있는 흐름과 구조를 지켜나가는 것이라고 본다.
'Develop > Web' 카테고리의 다른 글
[React] Tailwind-css를 써보면서 느낀점 (0) | 2025.02.20 |
---|---|
[React][Error] tailwind css 설치 오류 (1) | 2025.02.08 |
[JS] CommonJS와 ES모듈 (0) | 2024.12.13 |
[React][개발기] CI/CD 도입 (0) | 2024.08.08 |
[React][개발기] 10. 무한 슬라이드 개발 (0) | 2024.07.23 |