프론트엔드 코딩 컨벤션, 프론트엔드 네이밍 컨벤션, 프론트엔드 폴더구조 이런 부분들은 항상 고민이 많이 되는 영역인 것 같습니다.
대체로 많이 사용하는 것들이 정해져는 있는 것 같지만 특히 폴더구조의 경우에는 프로젝트의 성격과 유형에 따라 어떤 구조가 좋을지가 항상 다르죠
이게 머리로 이렇게 하니까 좋았었지라는 생각만 갖고 쓰다보니 저도 제 컨벤션이 헷갈리기 시작해서 이번기회에 제가 코드를 짜면서 자주 사용하는 컨벤션들을 정리해보았습니다.
쌍따옴표, 들여쓰기와 같이 포매터의 지원을 받는 부분의 경우에는 생략합니다.
프론트엔드 코딩 컨벤션 함수명 | 변수명
일반
1. 모든 변수,함수명은 소문자 카멜케이스를 통해 정의한다.
const helloWorld = () => {}
const hello = "hi"
2. 배열 변수의 경우 변수명의 끝에 List를 붙인다.
const helloList = ['hello' , 'world']
3. 타입이 boolean인 변수의 이름은 is , has , can으로 시작한다.
const isLoading = false
4. 클래스 등 생성자는 파스칼케이스를 사용한다.
class Hello {}
5. 상수는 대문자 스네이크 케이스를 사용한다.
const HELLO_WORLD = 'helloWorld'
6. Enum의 경우는 다음과 같은 형태를 따른다.
enum HI {
HelloWorld = 1,
}
7. 상수값을 모은 객체의 경우 다음과 같은 형태를 따른다.
const HELLO = {
hiWorld: "hiWorld",
};
8. 반환값이 boolean인 판별함수의 이름은 is, has, can으로 시작한다.
const isClient = () => {}
9. 무언가를 생성하는 함수 , 메서드는 create 접두사를 붙인다.
const createHelloWorld = () => {}
10. 입력을 특정한 형태로 변환하는 함수는 convert 접두사를 붙인다.
const convertHello = () => {}
11. 입력에서 특정한 값을 가져와서 반환하는 함수는 get 접두사를 붙인다.
const getHello = () => {}
12. 무언가를 여는 일을 하는 함수는 open 접두사를 붙인다. (자주 쓸 일 없음)
const openHello = () => {}
13. 무언가를 더하거나, 뺴는 함수는 add, minus 접두사를 붙인다.
const addHello = () => {}
const minusHello = () => {}
14. 배열을 입력받아 필터링된 결과물을 반환하는 함수는 filter 접두사를 붙인다.
const filterHello = (array) => {}
15. 배열을 입력받아 특정 요소를 찾아서 반환하는 함수는 find 접두사를 붙인다.
const findHello = (array) => {}
16. 배열을 입력받아 요소들을 변환하여 반환하는 함수는 convert 접두사를 붙인다.
const convertHello = (array) => {}
17. 관행적으로 사용되는 유틸함수의 이름은 관행을 따른다.
const throttle = () => {}
const commaize = () => {}
const decommaize = () => {}
18. ETC : 일반적인 케이스에 부합되지 않는 함수는 되도록 동사접두사를 가지는 형태로 작성한다.
const parseHello = () => {}
리액트
1. 리액트의 커스텀훅은 use 접두사를 붙인다.
const useCustomHook = () => {}
사실 강제되는 룰이라서 지킬수밖에 없긴합니다.
2. 고차컴포넌트의 경우 with 접두사를 붙인다.
const withAuth = () => {}
3. 이벤트핸들러는 handle 접두사 , (이벤트 유형) 접미사를 사용한다
const handleHiClick = () => {}
원래는 on 접두사를 사용했는데 doSomething 원칙에 어긋난다는 의견을 듣고 handle로 전향했습니다.
4. context의 경우 항상 접미사로 Context를 붙인다
const HelloContext = createContext(null)
5. context를 사용하는 훅인 경우 항상 접미사로 Context를 붙인다
const useHelloContext = () => {}
6. children을 반환하는 프로바이더 성격의 컴포넌트는 항상 접미사로 Provider를 붙인다
const HelloContextProvider = ({children]) => {}
7. 모든 프로바이더를 래핑하여 반환하는 프로바이더는 Providers라는 이름을 사용한다.
const Providers = ({children}) = > {}
프론트엔드 타입 네이밍 컨벤션
일반
1. type과 interface는 자유롭게 사용하되 type을 우선하여 사용한다.
type Hello = {hi:string} & {hello:string}
취향차이가 좀 있는 영역인 것 같은데 type이 좀 더 유연하게 타입들을 만들 수 있어서 저는 type 사용을 선호합니다.
2. 일반적인 경우 type 접미사를 사용한다.
bad
type User = {}
good
type UserType = {}
User와 같은 이름을 사용하게되면 실제 구현체와 이름이 같아서 import 할때 이름을 바꿔줘야하는 등의 불편이 생기는 경험이 있었습니다.
이런 경우를 방지하기 위해 Type의 경우 구분할 수 있도록 type 접미사를 사용합니다.
3. 타입은 파스칼 케이스로 작성한다.
bad
type helloWorldType = {}
good
type HelloWorldType = {}
4. 유틸리티 타입의 경우 Type 접미사를 사용하지 않는다.
export type OmitFromPick<T, U> = {
[K in Exclude<keyof T, keyof U>]: T[K];
};
리액트 관련
1. 컴포넌트의 Props에 대한 타입을 정의하는 경우 항상 접미사로 Props를 붙인다
type HelloProps = {}
2. 리액트 타입은 개별로 임포트하여 사용한다.
bad
React.ReactNode
good
ReactNode
프론트엔드 폴더구조 | 파일명 컨벤션
1. 모든 폴더, 파일명은 케밥케이스를 사용합니다.
good
hello-world
bad
hellWorld
2. 아래 표와 같이 약속된 subpath를 사용한다.
사용하는 서브패스명 | 의미 |
*.model.ts | 모델에 대한 정의가 들어갑니다. |
*.schema.ts | 간혹 zod 등의 스키마 밸리데이션이 필요한 경우 작성합니다. |
*.store.ts | atom 등 스토어와 관련된 코드를 넣습니다. |
*.action.ts | store에 대한 액션이 필요한 경우 사용합니다. |
*.factory.ts | react-query의 querykey factory를 배치하는 용도로 사용합니다. |
*.query.ts | react-query의 query 논리를 작성할 때 사용합니다. |
*.mutation.ts | react-query의 mutation 논리를 작성할 때 사용합니다. |
*.test.ts | 테스트 코드에 대한 약속된 subpath입니다. |
*.page.tsx | 페이지에 대한 컴포넌트를 작성할 때 사용합니다. |
*.constant.ts | 상수에 대한 정의를 할 때 사용합니다. |
*.util.ts | 해당 논리에서만 사용하는 유틸함수를 정의할 때 사용합니다. |
*.container.tsx | container-present 패턴을 사용해야하는 경우 container에 사용합니다. |
*.present.tsx | container-present 패턴을 사용해야하는 경우 present에 사용합니다. |
*.loading.tsx | 로딩과 관련된 컴포넌트에 사용합니다. |
*.type.ts | 유틸리티 타입, 혹은 모델에 해당하지는 않지만 타입만 정의한 파일의 경우 사용합니다 |
저는 코로케이션을 중요시하며 키보드로 파일명 탐색을 자주하다보니 subpath를 적극적으로 활용하는 편입니다.
익숙해지면 코딩하는게 꽤 빨라지더라구요
3. subpath는 1중첩 이상 사용하지 않는다.
그런데 이 원칙을 지키다보면 이런 모순이 발생하게 됩니다.
hello.util.ts를 테스트하려는데 그러면 hello.util.ts의 테스트코드파일은 hello.util.test.ts인가?
-> 이러한 경우 hello-util.test.ts로 작성하여 subpath 중첩이 1을 초과하는 경우를 피합니다.
4. 폴더구조는 다음과 같은 형태로 사용한다.
저같은 경우 Feature-Sliced Design의 철학을 제맘대로 조금씩 변형해서 사용합니다.
제가 느꼈을 때에 Feature Sliced Design은 entiities 와 feature를 나눠서 보는 과정에서 밀접하게 관련있는 코드들이 서로 거리가 멀리 떨어지게 되는 문제가 발생한다고 느꼈어요
물론 엄격하게 준수하면서 코드 짜는게 좋은 경우도 있지만 제가 하는 프로젝트 수준에서는 오히려 방해가 되는 경우가 많았습니다.
예를 들면 일반적으로 FSD를 사용할 때엔 react-query의 mutation에 관한 로직들을 features에 배치하는 경우가 많지만 이렇게하는 경우 프로젝트가 조금씩 커질 수록 쿼리, 모델과 뮤테이션간의 거리가 기하급수적으로 멀어져서 파일간의 컨텍스트 스위칭이 매우 힘들어졌었어요
그래서 우선은 다음과 같은 원칙을 통해 코드를 작성하고 있습니다.
shared | 1. 범용적으로 사용 가능한 코드 2. 모든 부분에게 의존당해야하는 코드 두가지 케이스를 배치합니다. |
entities | FSD는 model(store,actions) | api | ui | lib(선택 depth안에는 utils/hooks) 으로 배치합니다. 이것을 따르되 entities의 api에 해당 entities의 query, mutation 논리를 모두 포함합니다. |
features | FSD는 사용자 상호작용과 관련한 UI, 상호작용에 대한 논리가 포함된 코드를 배치할 것을 권합니다. 즉 기능과 엔티티를 분해할 것을 권하지만 충분히 커다란 프로젝트가 아닌 이상은 이득보다 오버헤드가 컸습니다. 따라서 features를 최대한 사용하지 않고 entities에서 해결합니다. 이렇게하면 엔티티와 기능이 섞이는 것처럼 보이지만 사실 엔티티 논리는 주로 model, *.query.ts 쪽에 배치되는 반면 feature 논리는 일반적으로 ui와 lib에 배치되기 때문에 향후 strict하게 FSD를 준수하기 위해 구조를 옮기는것도 어렵지 않습니다. |
widgets | features와 비슷한 논리로 최대한 사용을 자제하고자 노력합니다. |
pages | 명료하기 때문에 그냥 따르는 편입니다. 다만 다른 논리에 의해 사용되어질 필요가 있는 페이지는 pages 계층에 배치되면 FSD 규칙을 어기게됩니다. shared 계층에 이러한 page들을 위한 폴더를 만들거나 특정 케이스에 한해 규칙을 위반해야합니다. |
app | 명료하기 때문에 그냥 따르는 편입니다. |
항상 저렇게 많은 경우를 다 사용하는 것은 아니지만 보통 이런 형태로 귀결되는 것 같습니다.
처음에는 api | model | ui | lib 정도만 depth를 만들어두고 처리하다가 해당 엔티티가 성장하면서 점점 해당 폴더의 크기가 커지게되면 그때 api | model | ui | lib에 세부뎁스를 만들어주는 식으로 하면 괜찮더라구요
또 위 예제까지는 그래도 테스트코드들이 많은 편은 아니지만 테스트코드의 경우도 작성해야하는 소요가 많이 늘어나게 되면 테스트코드들을 찾아다니는게 오히려 불편해지는 경험도 있었습니다. 그런 경우에는 이런형태로 바꾸는 편입니다.
각 세부 폴더 안에 이름을 기준으로 depth를 하나 더 만들며 테스트와 원본코드를 한 폴더 안에서 묶어줍니다.
다만 보통 제가 다루는 프로젝트 수준에서는 이정도까지 필요했던 폴더는 많이 없었어서 이전 사진으로도 충분했던 것 같아요
이렇게 subpath를 활용하는 형태로 파일명을 지어주면 매우 큰 장점이 검색이 용이해진다는 것입니다.
다음과 같이 도메인에 대한 키워드 하나만 기억하고 있으면 바로 해당 도메인과 관련된 곳으로 이동할 수 있습니다.
또한 서브패스만 보고도 내가 수정하고 싶은 파일인지 아닌지 예측할 수 있어요
여기서 내가 수정하고 싶은 케이스가 명확하다고 가정하면 이렇게 이동할 수 있습니다.
저같은 경우에는 이처럼 명확하게 검색하고 바로 이동할 수 있어지니 컨텍스트 스위칭에 소모하는 시간이 많이 줄었어요
마치며
사실 폴더구조나 네이밍, 코딩 규칙과 같은 부분들은 아무래도 취향을 많이 타는 부분이다보니 제 방식이 싫은 분들도 있을 것 같습니다.
그냥 제가 자주 사용하는 방식들을 소개하는 글 정도로 봐주시면 좋을 것 같아요
읽어주셔서 감사합니다.
'frontend' 카테고리의 다른 글
변경에 자유로운 프론트엔드 코드 작성하기 (1) | 2024.05.25 |
---|---|
프론트엔드 테스트 환경 설정하기 (0) | 2024.05.22 |
next.js mdx fastrefresh not working problem (4) | 2024.04.21 |
리액트 컴포넌트 라이브러리를 빌드하고 배포하는 방법 (5) | 2024.03.17 |
늦은 밤에 도메인 죽은 썰 푼다 (1) | 2024.01.19 |