본 문서는 ONE 동아리 웹사이트(React + Three.js)의 프론트엔드 코드 컨벤션을 정의합니다. 다음 기수에게 인수인계되는 프로젝트이므로, **"내가 처음 보는 사람이라고 생각하고 짠다"**를 원칙으로 합니다. 컨벤션 수정이 필요한 경우 FE PM과 논의 후 PR로 반영합니다.
- 기술 스택
- 프로젝트 폴더 구조
- 네이밍 규칙
- 컴포넌트 작성 규칙
- 스타일링 규칙
- Three.js 작성 규칙
- 상태 관리 및 API 통신
- Import 순서
- 주석 작성 규칙
- Git 컨벤션
- 환경 변수 관리
- 코드 품질 도구
- 인수인계 체크리스트
| 분류 | 사용 기술 |
|---|---|
| Framework | React 18+ (Vite 기반) |
| Language | JavaScript (ES2022+) 또는 TypeScript |
| 3D | Three.js, @react-three/fiber, @react-three/drei |
| Routing | react-router-dom v6 |
| State | Zustand (전역), useState/useReducer (지역) |
| HTTP | Axios |
| Styling | Tailwind CSS (선택) 또는 CSS Modules |
| Lint/Format | ESLint + Prettier |
버전 변경 시 PM 승인 필수.
package.json의존성을 임의로 업그레이드하지 않습니다.
src/
├── apis/ # API 호출 함수 (axios)
│ ├── instance.js # axios 인스턴스 설정
│ ├── auth.js
│ └── recruit.js
├── assets/ # 이미지, 폰트, 3D 모델 파일
│ ├── images/
│ ├── fonts/
│ └── models/ # .glb, .gltf 등
├── components/ # 재사용 컴포넌트
│ ├── common/ # 공통 UI (Button, Input, Modal 등)
│ └── layout/ # Header, Footer, Sidebar 등
├── pages/ # 라우트 단위 페이지
│ ├── visitor/ # 방문자 페이지
│ │ ├── HomePage.jsx
│ │ ├── RecruitPage.jsx
│ │ ├── SchedulePage.jsx
│ │ └── AboutPage.jsx
│ └── admin/ # 관리자 페이지
│ ├── ApplicantPage.jsx
│ ├── AwardPage.jsx
│ └── ...
├── three/ # Three.js 관련 코드
│ ├── scenes/
│ ├── models/
│ └── utils/
├── hooks/ # 커스텀 훅 (useXxx)
├── stores/ # Zustand 스토어
├── styles/ # 전역 스타일
├── utils/ # 순수 유틸 함수
├── constants/ # 상수 (라우트 경로, 색상 등)
├── App.jsx
└── main.jsx
규칙
- 한 폴더에 파일이 10개 이상 쌓이면 하위 폴더로 분리합니다.
pages/안에는 라우트와 1:1로 매칭되는 파일만 둡니다. 그 외 로직은components/,hooks/로 분리합니다.
| 대상 | 규칙 | 예시 |
|---|---|---|
| 컴포넌트 파일 | PascalCase + .jsx |
RecruitForm.jsx |
| 일반 JS 파일 | camelCase + .js |
formatDate.js |
| 커스텀 훅 | use로 시작, camelCase |
useRecruitForm.js |
| 상수 | UPPER_SNAKE_CASE | MAX_FILE_SIZE |
| 변수/함수 | camelCase | getUserInfo |
| Boolean 변수 | is, has, can, should 접두사 |
isLoading, hasError |
| 이벤트 핸들러 | handle + 동작 |
handleSubmit, handleClick |
| props로 받는 핸들러 | on + 동작 |
onSubmit, onClose |
| CSS 클래스 (CSS Modules) | camelCase | submitButton |
금지 사항
- 의미 없는 약어 사용 금지 (
btn,usr,tmp등).button,user,temporary로 풀어쓰기. - 한글 변수명 금지.
data,info,value처럼 모호한 이름 단독 사용 금지.userData,recruitInfo처럼 맥락 포함.
- 함수형 컴포넌트만 사용합니다. Class 컴포넌트 금지.
- 한 파일에 한 컴포넌트. export default는 파일당 하나.
- 150줄을 넘으면 분리를 고민합니다. 250줄을 넘으면 무조건 분리합니다.
- props는 5개를 넘기지 않습니다. 넘으면 객체로 묶거나 컴포넌트 설계를 다시 봅니다.
// src/components/common/RecruitForm.jsx
import { useState } from 'react';
import { submitRecruit } from '@/apis/recruit';
import Button from '@/components/common/Button';
/**
* 신입부원 모집 신청 폼
* @param {Function} onSuccess - 제출 성공 시 호출되는 콜백
*/
function RecruitForm({ onSuccess }) {
// 1. 상태 선언
const [name, setName] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
// 2. 이벤트 핸들러
const handleSubmit = async () => {
setIsSubmitting(true);
try {
await submitRecruit({ name });
onSuccess?.();
} catch (error) {
console.error('[RecruitForm] 제출 실패:', error);
} finally {
setIsSubmitting(false);
}
};
// 3. 렌더링
return (
<div>
<input value={name} onChange={(e) => setName(e.target.value)} />
<Button onClick={handleSubmit} disabled={isSubmitting}>
제출
</Button>
</div>
);
}
export default RecruitForm;- import
- 컴포넌트 함수 선언
- 상태(useState, useReducer)
- 외부 데이터 호출 훅(useQuery 등) 또는 useEffect
- 핸들러 함수
- 파생 변수 (계산된 값)
- 조건부 early return (로딩, 에러 등)
- JSX 반환
- export default
- 삼항 연산자는 1단까지만. 중첩 금지.
- 복잡한 분기는 변수로 빼거나 early return 사용.
// ❌ 나쁜 예
{isLoading ? <Loading /> : error ? <Error /> : data ? <List /> : <Empty />}
// ✅ 좋은 예
if (isLoading) return <Loading />;
if (error) return <Error />;
if (!data) return <Empty />;
return <List />;- 기본은 Tailwind CSS를 사용합니다. (선택한 경우)
- 복잡한 애니메이션, 글로벌 스타일은
styles/아래 CSS 파일로 분리. - 인라인 style은 동적 값(예:
transform: translateX(${x}px))에만 사용.
- 하드코딩 금지. 모든 색상은
tailwind.config.js또는src/constants/theme.js에 정의 후 사용.
// src/constants/theme.js
export const COLORS = {
primary: '#3B82F6',
secondary: '#F59E0B',
textPrimary: '#1F2937',
textSecondary: '#6B7280',
};- 모바일 우선(Mobile First). 기본 스타일은 모바일,
md:,lg:등으로 확장. - 브레이크포인트:
sm: 640px,md: 768px,lg: 1024px,xl: 1280px(Tailwind 기본값 유지)
@react-three/fiber를 통해 선언적으로 작성합니다. 명령형(new THREE.Scene())은 정말 필요한 경우에만.- Three.js 관련 모든 코드는
src/three/아래에 둡니다. - 3D 모델 파일은
assets/models/에 두고, 파일명은 kebab-case (hero-scene.glb).
// src/three/scenes/HeroScene.jsx
import { Canvas } from '@react-three/fiber';
import { OrbitControls, useGLTF } from '@react-three/drei';
import { Suspense } from 'react';
function ClubLogo() {
const { scene } = useGLTF('/models/club-logo.glb');
return <primitive object={scene} />;
}
function HeroScene() {
return (
<Canvas camera={{ position: [0, 0, 5], fov: 50 }}>
<ambientLight intensity={0.5} />
<directionalLight position={[10, 10, 5]} />
<Suspense fallback={null}>
<ClubLogo />
</Suspense>
<OrbitControls enableZoom={false} />
</Canvas>
);
}
export default HeroScene;- 메모리 누수 방지:
useEffectcleanup에서geometry.dispose(),material.dispose(),texture.dispose()호출. - 재사용 가능한 geometry/material은
useMemo로 감쌉니다. 매 렌더마다 새로 만들지 않습니다. - 3D 모델은
useGLTF.preload()로 사전 로드하여 초기 진입 지연을 줄입니다. - 모바일 대응: 그림자, 안티앨리어싱은 기본 false. 필요 시 디바이스 성능 감지 후 활성화.
- 모델 파일 크기는 5MB 이하 권장. Draco 압축 사용.
좌표, FOV, 회전 속도 같은 값은 컴포넌트 상단에 상수로 분리합니다.
const CAMERA_POSITION = [0, 0, 5];
const ROTATION_SPEED = 0.005;
const LIGHT_INTENSITY = 0.8;| 종류 | 도구 |
|---|---|
| 컴포넌트 내부 상태 | useState, useReducer |
| 전역 상태 (로그인 정보, 테마 등) | Zustand |
| 서버 상태 (API 응답 캐싱) | TanStack Query 권장 / 또는 직접 관리 |
모든 API는 src/apis/instance.js의 공통 인스턴스를 통해 호출합니다.
// src/apis/instance.js
import axios from 'axios';
const instance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000,
});
instance.interceptors.request.use((config) => {
const token = localStorage.getItem('accessToken');
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
export default instance;컴포넌트에서 직접 axios.get(...)을 호출하지 않습니다. 반드시 apis/ 폴더의 함수를 통해 호출합니다.
// src/apis/recruit.js
import instance from './instance';
export const submitRecruit = (data) => instance.post('/recruit', data);
export const getRecruitList = () => instance.get('/admin/recruit');- 모든 비동기 호출은
try-catch로 감쌉니다. - 사용자에게 보일 에러 메시지는 상수로 관리하고, 콘솔에는
[컴포넌트명] 에러내용형식으로 출력합니다.
다음 순서로 정렬하고, 그룹 사이에 빈 줄을 둡니다.
// 1. React 및 외부 라이브러리
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import axios from 'axios';
// 2. 절대 경로 내부 모듈 (별칭 사용)
import { submitRecruit } from '@/apis/recruit';
import useAuthStore from '@/stores/authStore';
import Button from '@/components/common/Button';
// 3. 상대 경로 내부 모듈
import RecruitFormField from './RecruitFormField';
// 4. 스타일, 에셋
import styles from './RecruitForm.module.css';
import logoImage from '@/assets/images/logo.png';경로 별칭: vite.config.js에 @를 src/로 설정. 상대 경로(../../../)는 같은 폴더 내에서만.
- "무엇을" 하는지가 아니라 "왜" 하는지를 적습니다. 코드만 봐서 알 수 있는 건 주석 달지 않습니다.
- 인수인계받는 사람을 생각하고 적습니다. "이건 ~ 때문에 이렇게 했다"가 핵심.
- 한국어로 작성. 영어 섞지 않기.
// ❌ 나쁜 예: 코드만 봐도 아는 내용
// name 상태를 빈 문자열로 초기화
const [name, setName] = useState('');
// ✅ 좋은 예: 의도와 이유 설명
// 백엔드에서 한글 이름만 받기 때문에 영문 입력 시 사전 차단함
const isValidKoreanName = (name) => /^[가-힣]{2,5}$/.test(name);
// ✅ 좋은 예: 임시 코드 명시
// TODO: 디자인 확정 후 실제 색상으로 교체 (탁진우, 2026.05.01)
const TEMP_BG_COLOR = '#CCCCCC';재사용 컴포넌트와 유틸 함수에는 JSDoc 권장.
/**
* 날짜 문자열을 "YYYY년 MM월 DD일" 형태로 변환
* @param {string} dateString - ISO 8601 형식의 날짜 문자열
* @returns {string} 한국어 형식의 날짜
*/
export function formatKoreanDate(dateString) { ... }main → 배포용 (PM만 머지)
develop → 개발 통합 브랜치
feature/* → 기능 개발 (예: feature/recruit-form)
fix/* → 버그 수정
refactor/*→ 리팩토링
<type>: <제목 (한국어, 명령형, 50자 이내)>
<본문 - 필요 시>
type 종류
| type | 용도 |
|---|---|
feat |
새 기능 추가 |
fix |
버그 수정 |
style |
코드 포맷팅, 세미콜론 등 (기능 변화 X) |
refactor |
리팩토링 (기능 변화 X) |
design |
UI/CSS 변경 |
docs |
문서 수정 |
chore |
빌드 설정, 패키지 매니저 등 |
예시
feat: 신입부원 모집 신청 폼 구현
fix: 모바일에서 Three.js 캔버스가 잘리는 문제 해결
docs: README에 환경 변수 설명 추가
- base 브랜치는 항상
develop.main에 직접 PR 금지. - 제목: 커밋 컨벤션과 동일한 형식
- 본문에 포함할 것:
- 작업 내용 요약
- 스크린샷 또는 화면 녹화 (UI 변경 시)
- 테스트 방법
- 관련 이슈 번호
- 리뷰어 1명 이상의 approve 후 머지.
- 머지 방식: Squash and merge
- 환경 변수는
.env파일에 두고, 절대 커밋하지 않습니다. (.gitignore에 추가) - Vite 환경 변수는 반드시
VITE_접두사로 시작. .env.example파일을 만들어 어떤 변수가 필요한지 다음 기수가 알 수 있게 함.
# .env.example
VITE_API_BASE_URL=http://localhost:8080
VITE_ADMIN_SECRET_KEY=your_key_here금지: API 키, 비밀번호, 토큰을 코드에 하드코딩하지 않습니다.
프로젝트 루트의 .eslintrc, .prettierrc를 따릅니다. 개인 설정으로 덮어쓰지 마세요.
필수 설정
// .prettierrc
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 100,
"arrowParens": "always"
}-
npm run lint통과 -
npm run format적용 -
console.log제거 (디버깅 후) - 사용하지 않는 import 제거
- 주석 처리된 옛날 코드 제거
- ESLint
- Prettier - Code formatter
- ES7+ React/Redux/React-Native snippets
- GitLens
다음 기수에게 넘기기 전에 반드시 확인할 항목입니다.
- 본 README가 최신 상태인지 확인
-
.env.example에 모든 환경 변수가 명시되어 있는지 확인 - 주요 디렉토리에
README.md가 있는지 (예:src/three/README.md) - API 명세서 링크가 README에 포함되어 있는지
- 디자인 시안 링크 (Figma 등) 포함
-
TODO,FIXME주석 정리 (작성자/날짜 명시) - 사용하지 않는 파일/패키지 제거
- 매직 넘버 상수화
- 모든 페이지가 정상 동작하는지 수동 테스트
- 빌드 명령어 (
npm run build) 정상 동작 - 배포 URL, 도메인 정보 문서화
- 호스팅 계정 인수인계 (Vercel, Netlify 등)
- Firebase / 백엔드 서버 접근 권한 이양
- 알려진 버그/이슈 목록 작성
- 다음 기수에게 전하고 싶은 개선 포인트 정리
이 문서는 살아있는 문서입니다. 프로젝트가 진행되며 더 좋은 규칙이 발견되면 PR로 수정해 주세요. 규칙을 외우는 게 목적이 아니라, 다음에 이 코드를 볼 사람이 고통받지 않게 하는 것이 목적입니다.
문의: FE PM 탁진우 / BE PM 최예은
Last Updated: 2026.05.13