src/
├── app/ # 애플리케이션 설정 및 진입점
├── pages/ # 페이지 컴포넌트
├── widgets/ # 독립적인 UI 블록
├── features/ # 비즈니스 기능
├── entities/ # 비즈니스 엔티티
├── shared/ # 공유 리소스
- Features: 사용자 인터랙션과 비즈니스 로직 (로그인, 검색, 필터링 등)
- Widgets: 독립적인 UI 블록 (헤더, 사이드바, 카드 등)
| 상황 | 사용 경로 | 예시 |
|---|---|---|
| 동일 슬라이스 | 상대경로 | ./api, ../ui |
| 다른 슬라이스 혹은 다른 레이어 | 절대경로 | @/shared/ui |
- 동일 슬라이스: 같은 feature, widget, entity 폴더 내부에서의 import는 상대경로 사용
- 다른 슬라이스/레이어: features → shared, widgets → entities 등 다른 폴더간 import는 절대경로(@) 사용
- 장점: 코드 이동 시 상대경로는 자동으로 유지되고, 절대경로는 의존성을 명확히 표현
// 1. 라이브러리 코드
import React from 'react';
import { useState } from 'react';
// 2. 다른 레이어/슬라이스 (절대경로)
import { AppLayout } from '@/app/layouts';
import { HomePage } from '@/pages/home';
import { SearchWidget } from '@/widgets/search';
import { User } from '@/entities/user';
import { Button } from '@/shared/ui';
// 3. 동일 슬라이스 (상대경로)
import { authApi } from './api';
import { AuthForm } from '../ui';
import { useAuthValidation } from './hooks';- 컴포넌트: PascalCase (
UserCard.tsx) - 기타 파일: kebab-case (
user-api.ts,auth-hooks.ts)
- 컴포넌트: PascalCase (
UserCard/) - 기타 폴더: kebab-case (
user-api/,auth-hooks/)
// SomeComponent.tsx
export const SomeComponent = () => {
return <div>간단한 컴포넌트</div>;
};컴포넌트가 복잡해지면 폴더로 분리:
SomeComponent/
├── index.tsx # 메인 컴포넌트 (export default)
├── SomeComponentHeader.tsx # 헤더 섹션
├── SomeComponentContent.tsx # 컨텐츠 섹션
└── SomeComponentFooter.tsx # 푸터 섹션
// SomeComponent/index.tsx
import { SomeComponentHeader } from "./SomeComponentHeader";
import { SomeComponentContent } from "./SomeComponentContent";
import { SomeComponentFooter } from "./SomeComponentFooter";
export const SomeComponent = () => {
return (
<div>
<SomeComponentHeader />
<SomeComponentContent />
<SomeComponentFooter />
</div>
);
};// features/auth/index.ts
export { LoginForm } from './ui/LoginForm';
export { useAuth } from './hooks/use-auth';
export { authApi } from './api/auth-api';- 기본: named export 사용
- 컴포넌트 폴더의 index.tsx: export default 사용
React 컴포넌트 내부 코드는 다음 순서로 작성합니다:
const MyComponent = ({ prop1, prop2 }: Props) => {
// 1. 전역 상태 store들
const userStore = useUserStore();
const authStore = useAuthStore();
// 2. 로컬 상태 (useState 등)
const [localState, setLocalState] = useState('');
const [isLoading, setIsLoading] = useState(false);
// 3. 데이터 fetching 및 상태 관리 hooks
const { data: users, isLoading: usersLoading } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
// 4. 컴포넌트 초기화 로직 (optional)
useEffect(() => {
// 마운트 시 실행될 초기화 로직
initializeComponent();
}, []);
// 5. 계산된 값들 (useMemo)
const filteredUsers = useMemo(() => {
return users?.filter(user => user.name.includes(localState)) || [];
}, [users, localState]);
const displayConfig = useMemo(() => {
return {
showHeader: users && users.length > 0,
title: `총 ${users?.length || 0}개의 사용자`,
};
}, [users]);
// 6. 이벤트 핸들러 함수들 (useCallback)
const handleInputChange = useCallback((value: string) => {
setLocalState(value);
}, []);
const handleSubmit = useCallback(() => {
// 제출 로직
console.log('Submitted:', localState);
}, [localState]);
// 7. 사이드 이펙트 (useEffect)
useEffect(() => {
// 특정 상태 변경에 따른 사이드 이펙트
if (localState) {
trackUserInput(localState);
}
}, [localState]);
// 8. 조건부 렌더링 (early return)
if (usersLoading) {
return <LoadingSpinner />;
}
// 9. 메인 렌더링 로직
return (
<div>
<input value={localState} onChange={(e) => handleInputChange(e.target.value)} />
<button onClick={handleSubmit}>제출</button>
{filteredUsers.map(user => (
<UserCard key={user.id} user={user} />
))}
</div>
);
};- 전역 상태 먼저: 컴포넌트가 의존하는 외부 상태들을 명확히 보여줍니다
- 로컬 상태: 컴포넌트 내부에서만 사용되는 상태들
- 데이터 fetching: useQuery, useMutation 등 서버 상태 관리
- 초기화 로직: 컴포넌트 마운트 시 한 번 실행되는 로직
- 계산된 값: 상태에 의존하는 파생 데이터들
- 이벤트 핸들러: 사용자 인터랙션 처리 함수들
- 사이드 이펙트: 상태 변경에 따른 부수 효과들
- 조건부 렌더링: 로딩, 에러 상태 등의 early return
- 메인 렌더링: 실제 UI 렌더링 로직
const UserList = () => {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
fetchUsers().then(data => {
setUsers(data);
setLoading(false);
});
}, []);
return (
<div>
{loading ? <Spinner /> : users.map(user => <UserCard key={user.id} user={user} />)}
</div>
);
};// 렌더링 로직만 담당
const UserList = ({ users, loading }: UserListProps) => {
return (
<div>
{loading ? <Spinner /> : users.map(user => <UserCard key={user.id} user={user} />)}
</div>
);
};
// 데이터 페칭은 상위 컴포넌트에서 처리
const UserListContainer = () => {
const { users, loading } = useUsers();
return <UserList users={users} loading={loading} />;
};컴포넌트는 최대한 props를 통해 동작하도록 설계합니다.
// 책임이 명확한 컴포넌트
interface SearchBarProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
disabled?: boolean;
}
const SearchBar = ({ value, onChange, placeholder, disabled }: SearchBarProps) => {
return (
<input
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
disabled={disabled}
/>
);
};컴포넌트 내부에서 선언한 함수는 useCallback으로 감쌉니다.
const MyComponent = ({ onSubmit }: Props) => {
const [value, setValue] = useState('');
// ✅ useCallback으로 함수 메모이제이션
const handleSubmit = useCallback(() => {
onSubmit(value);
}, [value, onSubmit]);
const handleChange = useCallback((newValue: string) => {
setValue(newValue);
}, []);
return (
<form onSubmit={handleSubmit}>
<input value={value} onChange={(e) => handleChange(e.target.value)} />
</form>
);
};계산된 값이나 상수는 useMemo로 메모이제이션합니다.
const UserCard = ({ user, showDetails }: Props) => {
// ✅ 계산된 값 메모이제이션
const displayName = useMemo(() => {
return `${user.firstName} ${user.lastName}`;
}, [user.firstName, user.lastName]);
const userActions = useMemo(() => {
return showDetails ? ['edit', 'delete', 'view'] : ['view'];
}, [showDetails]);
return (
<div>
<h3>{displayName}</h3>
{/* 렌더링 로직... */}
</div>
);
};관련된 상태들은 객체로 묶어 응집도를 높입니다.
const [isSearchBarActive, setIsSearchBarActive] = useState(true);
const [searchBarValue, setSearchBarValue] = useState('');
const [searchBarPlaceholder, setSearchBarPlaceholder] = useState('');const [searchBarState, setSearchBarState] = useState({
isActive: true,
value: '',
placeholder: '검색어를 입력하세요',
});계산 가능한 값은 상태가 아닌 useMemo를 사용합니다.
const [placeholder, setPlaceholder] = useState('');
useEffect(() => {
if (searchBarState.isActive) {
setPlaceholder('검색어를 입력하세요');
} else {
setPlaceholder('검색이 비활성화되었습니다');
}
}, [searchBarState.isActive]);const placeholder = useMemo(() => {
return searchBarState.isActive
? '검색어를 입력하세요'
: '검색이 비활성화되었습니다';
}, [searchBarState.isActive]);useEffect보다는 이벤트 핸들러에서 직접 처리합니다.
useEffect(() => {
// 입력값이 변경될 때마다 실행되는 로직
validateInput(searchBarState.value);
trackUserInput(searchBarState.value);
}, [searchBarState.value]);const handleInputChange = useCallback((value: string) => {
setSearchBarState(prev => ({ ...prev, value }));
// 입력 변경 시 실행될 로직을 직접 호출
validateInput(value);
trackUserInput(value);
}, []);
return (
<input
value={searchBarState.value}
onChange={(e) => handleInputChange(e.target.value)}
/>
);프로젝트에서 적극 활용할 라이브러리들:
- lodash: 데이터 조작 유틸리티
- immer: 불변성 관리
- nuqs: URL 파라미터 관리
import { pick, omit, compact } from 'lodash';
// 객체에서 특정 키만 선택
const userBasicInfo = pick(user, ['id', 'name', 'email']);
// 특정 키 제외
const userWithoutSensitiveData = omit(user, ['password', 'ssn']);
// 배열에서 falsy 값 제거
const validItems = compact([1, 0, 'hello', '', null, 'world']);
// 결과: [1, 'hello', 'world']import { produce } from 'immer';
const updateUserProfile = useCallback((updates: Partial<User>) => {
setUser(
produce(draft => {
Object.assign(draft.profile, updates);
draft.updatedAt = new Date();
})
);
}, []);import { parseAsString, useQueryState } from 'nuqs';
const SearchPage = () => {
const [query, setQuery] = useQueryState('q', parseAsString.withDefault(''));
const [category, setCategory] = useQueryState('category', parseAsString.withDefault('all'));
return (
<div>
<SearchInput value={query} onChange={setQuery} />
<CategoryFilter value={category} onChange={setCategory} />
</div>
);
};이 컨벤션은 팀의 생산성과 코드 품질을 향상시키기 위한 가이드라인입니다. 새로운 요구사항이나 더 나은 방법을 발견하면 팀 논의를 통해 지속적으로 개선해 나가겠습니다.
eslint, prettier, vite, typescript, tailwindcss, shadcn, tanstack-query, zustand, react-router-dom