1. ESLint와 Prettier 사용하기

import/order 까지 설정해두면 복잡한 import 구문의 가독성을 조금이나마 개선할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// .eslintrc.json
{
...
"rules": {
"import/order": [
2,
{
"newlines-between": "always",
"groups": [
"builtin",
"external",
"internal",
"parent",
"sibling",
"index",
"unknown",
"object",
"type"
],
"alphabetize": {
"order": "asc",
"caseInsensitive": true
},
"pathGroups": [
{
"pattern": "react*",
"group": "external",
"position": "before"
}
]
}
]
}
}

2. 네이밍 컨벤션

컴포넌트, interface, type에는 PascalCase를 써라

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// React component
const BannersEditForm = () => {
...
}

// Typescript interface
interface TodoItem {
id: number;
name: string;
value: string;
}

// Typescript type alias
type TodoList = TodoItem[];

JavaScript 데이터(변수, 배열, 객체, 함수 등)은 camelCase를 써라

1
2
3
const getLastDigit = () => { ... }

const userTypes = [ ... ]

또한, 폴더와 컴포넌트가 아닌 파일 이름은 camelCase를 사용하고 컴포넌트 파일에는 PascalCase를 써라

1
2
3
src/utils/form.ts
src/hooks/useForm.ts
src/components/banners/edit/Form.tsx

3. TypeScript 통(barrels)을 사용해라

barrels는 여러 export를 하나의 파일에서 다루는 방법이다.

1
2
3
4
5
6
// barrel file example
export * from "./DropDown";
export * from "./TextBox";
export * from "./CheckBox";
export * from "./DateTimePicker";
export * from "./Slider";
1
2
3
4
5
6
7
import {
DropDown,
TextBox,
CheckBox,
DateTimePicker,
Slider,
} from "./src/controls";
  • 이렇게 하면 import를 여러 파일에서 하지 않고 하나의 파일에서 할 수 있어 간편하다.
  • 이렇듯 타입스크립트도 barrels를 사용하여 관리하면 클린 코드에 좋을 것이다.

4. 기본 내보내기 (default export)를 피하라

기본 내보내기는 내보낼 항목과 어떤 이름도 연결하지 않는다.

즉, 개발자가 내보내려는 이름대로 가져오는 것이 클린 코드에 보다 적합하다.

1
2
3
4
5
6
7
// ❌
export default MyComponent;

// ✅
export { MyComponent };
export const MyComponent = ...;
export type MyComponentType = ...;

5. 컴포넌트 구조 통일하기

모든 컴포넌트의 구조를 다음과 같이 통일해라

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
// 1. Imports - Prefer destructuring imports to minimize writen code
import React, { PropsWithChildren, useState, useEffect } from "react";

// 2. Types
type ComponentProps = {
someProperty: string;
};

// 3. Styles - with @mui use styled API or sx prop of the component
const Wrapper = styled("div")(({ theme }) => ({
color: theme.palette.white,
}));

// 4. Additional variables
const SOME_CONSTANT = "something";

// 5. Component
function Component({ someProperty }: PropsWithChildren<ComponentProps>) {
// 5.1 Definitions
const [state, setState] = useState(true);
const { something } = useSomething();

// 5.2 Functions
function handleToggleState() {
setState(!state);
}

// 5.3 Effects
// ❌
React.useEffect(() => {
// ...
}, []);

// ✅
useEffect(() => {
// ...
}, []);

// 5.5 Additional destructures
const { property } = something;

return (
<div>
{/* Separate elements if not closed on the same line to make the code clearer */}
{/* ❌ */}
<div>
<div>
<p>Lorem ipsum</p>
<p>Pellentesque arcu</p>
</div>
<p>Lorem ipsum</p>
<p>Pellentesque arcu</p>
</div>
<div>
<p>
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Pellentesque
arcu. Et harum quidem rerum facilis est et expedita distinctio.
</p>
<p>Pellentesque arcu</p>
<p>
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Pellentesque
arcu. Et harum quidem rerum facilis est et expedita distinctio.
</p>
</div>

{/* ✅ */}
<Wrapper>
<div>
<p>Lorem ipsum</p>
<p>Pellentesque arcu</p>
</div>

<p>Lorem ipsum</p>
<p>Pellentesque arcu</p>
</Wrapper>

<div>
<div>
<p>
Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
Pellentesque arcu. Et harum quidem rerum facilis est et expedita
distinctio.
</p>

<p>Pellentesque arcu</p>

<p>
Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
Pellentesque arcu. Et harum quidem rerum facilis est et expedita
distinctio.
</p>
</div>
</div>
</div>
);
}

// 6. Exports
export { Component };
export type { ComponentProps };

6. PropsWithChildren 을 사용해라

1
2
3
4
5
6
7
8
9
10
11
12
13
import React, { PropsWithChildren } from "react";

type ComponentProps = {
someProperty: string,
};

// ✅
function Component({
someProperty,
children,
}: PropsWithChildren<ComponentProps>) {
// ...
}
  • props로 children을 내려주고 children 타입을 설정해주는 작업이 반복적으로 발생할 때 번거로움을 해소하고자 PropsWithChildren을 사용할 수 있다.

PropsWithChildren의 children 타입은 optional 하다. 그러므로 꼭 children이 들어가야하는 컴포넌트에서 보다 엄격하게 타입을 지정해주기 위해서는 children:ReactNode로 타입을 지정해주는 방법이 있다.

7. JSX에서 함수가 한줄 이상이라면 분리하라

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ❌
<button
onClick={() => {
setState(!state);
resetForm();
reloadData();
}}
/>

// ✅
<button onClick={() => setState(!state)} />

// ✅
const handleButtonClick = () => {
setState(!state);
resetForm();
reloadData();
}

<button onClick={handleButtonClick} />

8. Key props로 index를 사용을 피해라

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// ❌
const List = () => {
const list = ["item1", "item2", "item3"];

return (
<ul>
{list.map((value, index) => {
return <li key={index}>{value}</li>;
})}
</ul>
);
};

// ✅
const List = () => {
const list = [
{ id: "111", value: "item1" },
{ id: "222", value: "item2" },
{ id: "333", value: "item3" },
];

return (
<ul>
{list.map((item) => {
return <li key={item.id}>{item.value}</li>;
})}
</ul>
);
};

공시군서에 따르면 배열 내에서만 고유한 값을 전달해주면 된다고 나와있으니 map 고차함수를 사용하여 index를 key prop로 전달해도 될 것 같다.

하지만 이렇게 할 경우, React에서는 props가 변경되면 컴포넌트를 재렌더링하는데, 배열에서 리스트를 추가, 삭제하면 index가 변경되므로 변경되지 않은 다른 리스트들도 불필요한 재렌더링이 발생하게 된다.

그러므로 key prop에는 item의 id, react uid 라이브러리로 고유한 key를 지정해줘야한다.

9. fragments를 써라

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ❌
const ActionButtons = ({ text1, text2 }) => {
return (
<div>
<button>{text1}</button>
<button>{text2}</button>
</div>
);
};

// ✅
const Button = ({ text1, text2 }) => {
return (
<>
<button>{text1}</button>
<button>{text2}</button>
</>
);
};

불필요한 div 태그 대신 Fragment를 사용하자

10. 구조분해할당 사용하라

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ❌
const Button = (props) => {
return <button>{props.text}</button>;
};

// ✅
const Button = (props) => {
const { text } = props;

return <button>{text}</button>;
};

// ✅
const Button = ({ text }) => {
return <button>{text}</button>;
};

11. 관심사를 분리해라

presentation 컴포넌트에서 business 로직을 분리하는 것은 컴포넌트 코드의 가독성을 높힐 수 있다.

대부분의 page, screen, container 컴포넌트에 다수의 hook과 useEffect를 사용하려고 할 때 business 로직을 분리하는 것을 시도할 수 있다.

custom hook

관심사(책임)을 분리하기 위해서 useEffect나 다수의 useState를 컴포넌트에 직접 넣는 대신 Custom hook을 사용해라

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// ❌
const ScreenDimensions = () => {
const [windowSize, setWindowSize] = useState({
width: undefined,
height: undefined,
});

useEffect(() => {
function handleResize() {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
}

window.addEventListener("resize", handleResize);
handleResize();

return () => window.removeEventListener("resize", handleResize);
}, []);

return (
<>
<p>Current screen width: {windowSize.width}</p>
<p>Current screen height: {windowSize.height}</p>
</>
);
};

// ✅
const useWindowSize = () => {
const [windowSize, setWindowSize] = useState({
width: undefined,
height: undefined,
});

useEffect(() => {
function handleResize() {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
}

window.addEventListener("resize", handleResize);
handleResize();

return () => window.removeEventListener("resize", handleResize);
}, []);

return windowSize;
};

const ScreenDimensions = () => {
const windowSize = useWindowSize();

return (
<>
<p>Current screen width: {windowSize.width}</p>
<p>Current screen height: {windowSize.height}</p>
</>
);
};

12. 거대 컴포넌트를 피해라

거대 컴포넌트가 가능하더라도, 컴포넌트를 작은 단위로 분리해라.

주로 조건부 렌더링을 할 때 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// ❌
const SomeSection = ({ isEditable, value }) => {
if (isEditable) {
return (
<Section>
<Title>Edit this content</Title>
<Content>{value}</Content>
<Button>Clear content</Button>
</Section>
);
}

return (
<Section>
<Title>Read this content</Title>
<Content>{value}</Content>
</Section>
);
};

// ✅
const EditableSection = ({ value }) => {
return (
<Section>
<Title>Edit this content</Title>
<Content>{value}</Content>
<Button>Clear content</Button>
</Section>
);
};

const DetailSection = ({ value }) => {
return (
<Section>
<Title>Read this content</Title>
<Content>{value}</Content>
</Section>
);
};

const SomeSection = ({ isEditable, value }) => {
return isEditable ? (
<EditableSection value={value} />
) : (
<DetailSection value={value} />
);
};

13. 가능하다면 state를 그룹화해라

1
2
3
4
5
6
// ❌
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");

// ✅
const [user, setUser] = useState({});

14. boolean shorthand를 사용해라

1
2
3
4
5
// ❌
<Form hasPadding={true} withError={true} />

// ✅
<Form hasPadding withError />

15. curly braces를 피해라

1
2
3
4
5
// ❌
<Title variant={"h1"} value={"Home page"} />

// ✅
<Title variant="h1" value="Home page" />

16. inline 스타일을 피해라

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// ❌
const Title = (props) => {
return (
<h1 style={{ fontWeight: 600, fontSize: "24px" }} {...props}>
{children}
</h1>
);
};

// ✅
const useStyles = (props) => {
return useMemo(
() => ({
header: { fontWeight: props.isBold ? 700 : 400, fontSize: "24px" },
}),
[props]
);
};

const Title = (props) => {
const styles = useStyles(props);

return (
<h1 style={styles.header} {...props}>
{children}
</h1>
);
};

17. 조건부 렌더링은 삼항 연산자 사용해라

1
2
3
4
5
6
7
8
9
10
11
const { role } = user;

// ❌
if (role === ADMIN) {
return <AdminUser />;
} else {
return <NormalUser />;
}

// ✅
return role === ADMIN ? <AdminUser /> : <NormalUser />;

18. 타입 별칭을 사용해라

1
2
3
4
5
6
7
8
9
10
11
export type TodoId = number;
export type UserId = number;

export interface Todo {
id: TodoId;
name: string;
completed: boolean;
userId: UserId;
}

export type TodoList = Todo[];

19. 써드 파티 라이브러리를 직접 사용하는 것을 피해라

1
2
// src/lib/store.ts
export { useDispatch, useSelector } from "react-redux";
1
2
// src/lib/query.ts
export { useQuery, useMutation, useQueryClient } from "react-query";

20. 직접 구현 대신 추상화에 의존해라

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// ❌ directly using momemt
import moment from "moment";

const updateProduct = (product) => {
const payload = {
...product,
// ❌ we are bound to the moment interface implementation
updatedAt: moment().toDate(),
};

return await fetch(`/product/${product.id}`, {
method: "PUT",
body: JSON.stringify(payload),
});
};

// ✅ creating the abstraction, a.k.a. helper function which wraps the functionality

// utils/createDate.ts
import moment from "moment";

export const createDate = (): Date => moment().toDate();

// updateProduct.ts
import { createDate } from "./utils/createDate";

const updateProduct = (product) => {
const payload = {
...product,
// ✅ using the abstracted helper function
updatedAt: createDate(),
};

return await fetch(`/product/${product.id}`, {
method: "PUT",
body: JSON.stringify(payload),
});
};
  • moment를 사용하여 직접 구현하기 보다 createDate 함수사용하여 추상화하면 간결하다.

21. 선언적 프로그래밍을 해라

1
2
3
4
5
6
7
8
9
10
11
// ❌ imperative: dealing with internals of array iteration
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
let sum = 0;

for (let i = 0; i < arr.length; i++) {
sum += arr[i];
}

// ✅ declarative: we don't deal with internals of iteration
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
const sum = arr.reduce((acc, v) => acc + v, 0);

22. 변수 이름을 이쁘게 지어라

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ❌ Avoid single letter names
const n = "Max";
// ✅
const name = "Max";

// ❌ Avoid abbreviations
const sof = "Sunday";
// ✅
const startOfWeek = "Sunday";

// ❌ Avoid meaningless names
const foo = false;
// ✅
const appInit = false;

23. 함수 인자를 3개 이상 넘기지 말아라

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ❌
function createPerson(firstName, lastName, height, weight, gender) {
// ...
}

// ✅
function createPerson({ firstName, lastName, height, weight, gender }) {
// ...
}

// ✅
function createPerson(person) {
const { firstName, lastName, height, weight, gender } = person;
// ...
}

24. template literal 사용해라

1
2
3
4
5
// ❌
const userName = user.firstName + " " + user.lastName;

// ✅
const userDetails = `${user.firstName} ${user.lastName}`;

25. 간단한 함수에서 암묵적 return 사용해라

1
2
3
4
5
6
7
// ❌
const add = (a, b) => {
return a + b;
};

// ✅
const add = (a, b) => a + b;

참고

React Code Conventions and best practices - Medium