React Query란?

카테고리 React

React Query란?

client에서 상태관리 라이브러리를 사용하는 것은 클라이언트에서만 유효하다.

서버의 데이터를 요청하고 클라이언트의 전역 상태로 갱신하는 로직이 추가되면 클라이언트의 상태관리 라이브러리 코드가 복잡해지는 문제가 생긴다.

즉, 리액트 쿼리는 서버 데이터와 클라이언트 데이터를 구분하기 위해 사용한다.

useQuery

컴포넌트나 custom hook에서 query를 구독하기 위해서는 useQuery를 호출해야한다.

이 때, 유니크한 queryKey와 promise를 반환하는 queryFn가 있어야 한다.

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
import { useQuery } from "@tanstack/react-query";

function App() {
const info = useQuery({ queryKey: ["todos"], queryFn: fetchTodoList });
}

function Todos() {
const { isLoading, isError, data, error } = useQuery({
queryKey: ["todos"],
queryFn: fetchTodoList,
});

if (isLoading) {
return <span>Loading...</span>;
}

if (isError) {
return <span>Error: {error.message}</span>;
}

return (
<ul>
{data.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}
  • useQuery의 반환값은 데이터를 사용하기 위한 모든 정보를 담고 있다.
  • queryKey: query caching을 queryKey를 통해서 관리한다.
    • 쿼리키는 최상위 레벨이어야 하고, string으로 구성된 배열이어야 한다.
  • queryKey에는 쿼리함수에서 사용되는 모든 변수가 포함되어야 한다.
1
2
3
4
5
6
function Todos({ todoId }) {
const result = useQuery({
queryKey: ["todos", todoId],
queryFn: () => fetchTodoById(todoId),
});
}
  • queryFn: 데이터를 resolve(분해)하거나 에러를 던지는 Promise를 반환하는 함수
    • 쿼리가 에러를 가지고 있다는 것을 결정하려면, 쿼리함수가 throw Error 또는 rejected Promise를 반환해야한다.

useMutation

query와 달리 mutation은 데이털를 생성, 갱신, 삭제하거나 서버에서 사이드 이펙트를 수행할 때, 사용된다.

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
function App() {
const mutation = useMutation((newTodo) => {
return axios.post("/todos", newTodo);
});

return (
<div>
{mutation.isLoading ? (
"Adding todo..."
) : (
<>
{mutation.isError ? (
<div>An error occurred: {mutation.error.message}</div>
) : null}

{mutation.isSuccess ? <div>Todo added!</div> : null}

<button
onClick={() => {
mutation.mutate({ id: new Date(), title: "Do Laundry" });
}}
>
Create Todo
</button>
</>
)}
</div>
);
}

만약 mutation 중 error가 발생하여 data를 비우고 싶을 땐 reset을 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const CreateTodo = () => {
const [title, setTitle] = useState("");
const mutation = useMutation({ mutationFn: createTodo });

const onCreateTodo = (e) => {
e.preventDefault();
mutation.mutate({ title });
};

return (
<form onSubmit={onCreateTodo}>
{mutation.error && (
<h5 onClick={() => mutation.reset()}>{mutation.error}</h5>
)}
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<br />
<button type="submit">Create Todo</button>
</form>
);
};

mutation의 사이드이펙트 처리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
useMutation({
mutationFn: addTodo,
onMutate: (variables) => {
// mutation이 발생할 때 호출

// 선택적으로 롤백 시 데이터를 포함하는 context를 반환할 수 있다.
return { id: 1 };
},
onError: (error, variables, context) => {
// 에러시 호출
console.log(`rolling back optimistic update with id ${context.id}`);
},
onSuccess: (data, variables, context) => {
// 성공시 호출
},
onSettled: (data, error, variables, context) => {
// 에러든, 성공이든 호출
},
});
  1. onMutate가 현재 mutate가 발생했을 때, 실행하는 함수이다. 위 코드에서는 롤백할 때, 사용할 데이터를 반환하고 있다.
  2. 만약 에러가 발생하면, 해당 업데이트를 롤백하는 데이터가 context에 포함되어있다.
  3. onSettled는 성공하든 실패하든 호출되는 함수다.

만약, 콜백함수에서 Promise를 반환하면, 다음 콜백이 호출되기 전에 대기한다.

1
2
3
4
5
6
7
8
9
useMutation({
mutationFn: addTodo,
onSuccess: async () => {
console.log("I'm first!");
},
onSettled: async () => {
console.log("I'm second!");
},
});
  • onSuccess가 먼저 발생하는 이유는 내부 로직때문이 아니라 단순히 코드상 위에 있기 때문이다.

댓글 공유

useReducer 예시

카테고리 React

useReducer hook은 상태관리 도구이다.

useState의 대안으로 많이 사용된다.

2개 이상의 상태를 관리하기 위해서 각각을 useState로 관리하는 것 보단 useReducer를 사용하여 action별로 상태 관리하는 것이 훨씬 단순하다.

문법

1
const [state, dispatch] = useReducer(reducer, initialState);
  • useReducer는 3개의 인자를 받을 수 있다.
  • reducer 함수, initialState(초기상태), initFunction(초기화함수, optional)

예시

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
import { useReducer } from "react";
const reducer = (state, action) => {
switch (action.type) {
case "INCREMENT":
return { ...state, count: state.count + 1 };
case "DECREMENT":
return { ...state, count: state.count - 1 };
case "USER_INPUT":
return { ...state, userInput: action.payload };
case "TOGGLE_COLOR":
return { ...state, color: !state.color };
default:
throw new Error();
}
};

function App() {
const [state, dispatch] = useReducer(reducer, {
count: 0,
userInput: "",
color: false,
});

return (
<main
className="App, App-header"
style={{ color: state.color ? "#000" : "#FF07FF" }}
>
<input
style={{ margin: "2rem" }}
type="text"
value={state.userInput}
onChange={(e) =>
dispatch({ type: "USER_INPUT", payload: e.target.value })
}
/>
<br />
<br />
<p style={{ margin: "2rem" }}>{state.count}</p>
<section style={{ margin: "2rem" }}>
<button onClick={() => dispatch({ type: "DECREMENT" })}>-</button>
<button onClick={() => dispatch({ type: "INCREMENT" })}>+</button>
<button onClick={() => dispatch({ type: "TOGGLE_COLOR" })}>
Color
</button>
</section>
<br />
<br />
<p style={{ margin: "2rem" }}>{state.userInput}</p>
</main>
);
}

export default App;
  • reducer 함수에는 초기값 stateaction에 따른 case별로 반환하는 상태값을 정의한다.
  • useReducer hook을 사용하여 state(상태), dispatch(상태변경함수)를 선언한다.
  • dispatch(상태변경함수)의 인자로 객체를 전달해주는데, type, payload 프로퍼티를 갖는 객체를 전달해준다.
  • action 객체의 type에 따라 reducer에 정의해둔 action case에 따라 반환하는 값이 달라진다.
  • 위와 같이 사용하면, useStateuseState 변경함수를 여러 개 정의하지 않고도 직관적으로 상태관리 코드를 작성할 수 있다는 장점이 있다.

댓글 공유

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

댓글 공유

리액트 무한 스크롤 라이브러리 사용해보기

react-infinite-scroller

위 라이브러리를 사용하여 리액트에서 무한 스크롤을 사용해보자.

1
2
3
4
npm i react-infinite-scroller

# typeScript를 사용한다면 추가
npm i --save-dev @types/react-infinite-scroller
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
import { useEffect, useState } from "react";
import InfiniteScroll from "react-infinite-scroller";

function App() {
const [data, setData] = useState<{ label: string }[]>([]);
// const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);

function handleLoadMore() {
console.log("more load!");
}

// if (loading) {
// return <div>Loading ...</div>;
// }

if (error) {
return <div>Error: {error}</div>;
}

useEffect(() => {
fetch(Markdown)
.then((res) => res.text())
.then((data) => setMarkdown(data));
}, [markdown]);

useEffect(() => {
// setLoading(true);
try {
setData(customAssetList);
// setLoading(false);
} catch (error) {
setError(error);
// setLoading(false);
}
});

return (
<div className="App">
<InfiniteScroll
// 사용자에 의해 많은 아이템이 요청될 실행하는 콜백함수
loadMore={handleLoadMore}
// 이벤트 리스너를 component의 부모요소에 추가
useWindow={false}
// 해당 아이템을 감싸는 태그
element="article"
className="index"
// loading 중일 보여줄 요소
loader={
<div className="loader" key={0}>
Loading ...
</div>
}
>
{data.map((da, index) => (
<div key={index}>
<h3>종목: {index + 1}</h3>
<p>{da.label}</p>
</div>
))}
</InfiniteScroll>
</div>
);
}
  • data를 state로 관리해주고 useEffect 훅을 사용하여 처음 화면이 렌더링 될 때, loading 상태를 변경시키고 data를 업데이트한다.
  • InfiniteScroll 컴포넌트는 내부적으로 loading 상태일 때를 관리하고 있어 개발자가 따로 loading state를 가질 필요가 없다.

검색 기능 활성화 시키기

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
import debounce from "lodash/debounce";

function App() {
const [data, setData] = useState<{ label: string }[]>([]);
const [searchValue, setSearchValue] = useState("");
const [filteredData, setFilteredData] = useState<{ label: string }[]>([]);

const handleSearchValue = debounce((value) => {
setSearchValue(value);
const filtered = data.filter((item) => item.label.includes(value));
setFilteredData(filtered);
}, 300);

const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const value = e.currentTarget.value;
handleSearchValue(value);
};

return (
<div>
<InfiniteScroll
loadMore={handleLoadMore}
useWindow={false}
element="article"
className="index"
loader={
<div className="loader" key={3}>
Loading ...
</div>
}
>
{searchValue !== ""
? filteredData.map((item, index) => (
<div key={index}>
<h3>종목: {index + 1}</h3>
<p>{item.label}</p>
</div>
))
: data.map((da, index) => (
<div key={index}>
<h3>종목: {index + 1}</h3>
<p>{da.label}</p>
</div>
))}
</InfiniteScroll>
</div>
);
}
  • handleChange를 onChange 이벤트 핸들러로 등록하여 change 이벤트가 발생할 때 마다 검색값으로 필터된 데이터를 보여준다.
  • onChange 이벤트가 너무 많이 발생하면 1만개의 데이터가 자주 렌더링 되므로 성능에 문제가 있을 수 있어 debounce를 사용하여 이벤트가 끝난 후 0.3초 후에 이벤트를 캐치하도록 구현

댓글 공유

React에서 TypeScript 올바르게 사용하기

Select 컴포넌트를 만든다고 가정해보자.

1
2
3
4
5
6
7
8
9
10
11
12
const Select = ({ label, value, options, onChange }) => {
return (
<label>
{label}
<select value={value} onChange={onChange}>
{options.map((option) => (
<option value={option.value}>{option.label}</option>
))}
</select>
</label>
);
};

1. 무지성으로 타입 정의

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const Select = ({
label,
value,
options,
onChange,
}: {
label: string;
value: string;
options: { label: string; value: string }[];
onChange: (event: React.ChangeEvent<HTMLSelectElement>) => void;
}) => {
return (
<label>
{label}
<select value={value} onChange={onChange}>
{options.map((option) => (
<option value={option.value}>{option.label}</option>
))}
</select>
</label>
);
};

2. type, interface로 타입 정의

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type Option = { label: string; value: string };

type Options = Option[];

type SelectProps = {
label: string;
value: string;
options: Options;
onChange: (event: React.ChangeEvent<HTMLSelectElement>) => void;
};

const Select = ({ label, value, options, onChange }: SelectProps) => {
return (
<label>
{label}
<select value={value} onChange={onChange}>
{options.map((option) => (
<option value={option.value}>{option.label}</option>
))}
</select>
</label>
);
};

3. 타입 파일 분리하기

1
2
3
4
5
6
7
8
9
10
11
// Select.d.ts
type Option = { label: string; value: string };

type Options = Option[];

type SelectProps = {
label: string;
value: string;
options: Options;
onChange: (event: React.ChangeEvent<HTMLSelectElement>) => void;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Select.tsx
const Select: React.FC<SelectProps> = ({
label,
value,
options,
onChange,
}): JSX.Element => {
return (
<label>
{label}
<select value={value} onChange={onChange}>
{options.map((option) => (
<option value={option.value}>{option.label}</option>
))}
</select>
</label>
);
};
  • 정답은 없지만 팀 내부에서 하나를 결정하고 통일할 필요는 있다.

댓글 공유

확장성 있는 Text 컴포넌트

1
2
<Text typography="h1">나는 머릿말 1이야</Text>
<Text typography="content">나는 콘텐츠야</Text>

위와 같은 컴포넌트를 만들기 위한 작업을 시작해보자.

컴포넌트 재사용을 높이기 위해서는 누구나 이해하기 쉽게 작성하는 것이 좋다. 그러기 위해서는 선언형 코드인 HTML 방식을 따르는 것이 이득이다.

HTML 처럼 만든 컴포넌트

1
2
3
4
type Props = HTMLAttributes<"span">;
const Text = (props: Props) => {
/* ... */
};

이런식으로만 사용해도 HTML Element를 똑같이 따라할 수 있다.

하지만 HTML 속성은 ref, key 같은 리액트 속성들을 없다.

이 때 사용하는 것이 forwardRef 함수로 ref 값은 전달해준다.

1
2
3
4
5
6
const Text = forwardRef(function Text(
props: ComponentPropsWithoutRef<"span">,
ref: Ref<HTMLSpanElement>
) {
return <span ref={ref}>{props.children}</span>;
});
  • ComponentPropsWithoutRef<"span">은 ref만 제외한 나머지 리액트 프로퍼티를 모두 포함한다.

커스텀 프로퍼티 추가하기

지금까지의 컴포넌트는 그저 span element와 동일하다. 다른 커스텀 프로퍼티도 받을 수 있도록 타입스크립트의 & (intersection)을 사용하여 타입을 확장해보자.

1
2
3
4
5
6
7
8
9
10
type TextProps = {
typography?: string;
} & ComponentPropsWithoutRef<"span">;

const Text = forwardRef(function Text(
{ typography, ...props }: Props,
ref: Ref<HTMLSpanElement>
) {
//...
});

하지만, ComponentPropsWithoutRef<"span">가 이미 갖고 있는 프로퍼티와 커스텀 프로퍼티가 동일한 경우 대응하기 어렵다.

1
2
3
type TextProps = {
customId?: number;
} & ComponentPropsWithoutRef<"span">;

예를 들어 위와 같은 커스텀 프로퍼티는 ComponentPropsWithoutRef<"span">와 동일한 프로퍼티가 없기 때문에 제대로 타입이 추론된다.

1
2
3
type TextProps = {
id?: number;
} & ComponentPropsWithoutRef<"span">;

하지만 id 프로퍼티는 ComponentPropsWithoutRef<"span">가 소유하고 있어 동일한 프로퍼티 2개가 생기게 되므로 타입스크립트가 제대로 추론하지 못하고 undefined로 추론된다.

이를 해결하기 위해서는 Omit 타입을 사용하여 오버라이딩하려는 프로퍼티를 먼저 제거한 후 병합해야지만 문제가 없다.

1
type Combine<T, K> = T & Omit<K, keyof T>;
  • Combine 유틸타입은 2개의 타입을 받은 후 K타입에서 T 타입이 가진 프로퍼티와 중복되는 프로퍼티를 제거한 후 & (intersection)으로 병합한다.

이런식으로 타입을 병합하려면 상당히 귀찮기 때문에 유틸 타입을 하나 만들어둔다.

원하는 요소로 렌더링 하기

지금까지는 span 요소로만 작동하기 때문에 확장성을 위해 ComponentPropsWithoutRef<"span"> 에서 span 위치에 변수를 할당하도록 구현한다.

1
2
3
4
5
6
7
8
9
// Text 컴포넌트의 커스텀 프로퍼티 선언
type TextBaseProps<T> = {
typography?: string;
as?: T;
};

// Props<T>는 ComponentPropsWithoutRef<T>에 이 값을 그대로 넘겨준다.
// 그리고 커스텀 프로퍼티 내부의 as에도 T 타입을 바인딩해준다.
type TextProps<T> = Combine<TextBaseProps<T>, ComponentPropsWithoutRef<T>>;

하지만, 이는 오류를 발생시킨다. ComponentPropsWithoutRef가 받을 수 있는 제네릭 타입이 ElementType으로 정해져 있기 때문이다.

그래서 type TElementType을 상속한 타입이여야 한다고 명시해야한다.

1
2
3
4
5
6
7
8
9
10
11
type TextBaseProps<T extends ElementType> = {
typography?: string;
as?: T;
};

type TextProps<T extends ElementType> = Combine<
TextBaseProps<T>,
ComponentPropsWithoutRef<T>
>;

function Text<T extends ElementType>(props: TextProps<T>) {}

이제 모든 type T가 동일하다는 것을 보장할 수 있다.

  1. Text<T>의 타입 변수 T
  2. TextProps<T>의 타입 변수 T
  3. TextBaseProps<T>의 타입 변수 T
  4. as 프로퍼티에 바인딩 된 타입 변수 T
  5. ComponentPropsWithoutRef<T>의 타입 변수 T

즉, 이 중 한곳이라도 T에 대해서 명확하게 알 수 있다면 나머지 부분에서도 자연스럽게 추론이 가능하다.

as 프로퍼티로 타이핑 추상화 하기

as라는 프로퍼티는 Text 컴포넌트 뿐만 아니라 다양한 컴포넌트에서도 사용될 수 있기에 이 부분을 최대한 추상화 해둘 필요가 있다.

1
2
3
4
5
6
7
8
// 텍스트 컴포넌트의 프로퍼티
type TextBaseProps = {
typography?: string;
};

// T 타입을 추론할 수 있는 as 프로퍼티를 자동으로 포함하고
// T 타입으로 HTML 엘리먼트 속성까지 타이핑 해주는 OverridableProps!
type TextProps<T extends ElementType> = OverridableProps<T, TextBaseProps>;
  • OverridableProps 타입은 특정 컴포넌트가 as 프로퍼티를 사용하여 HTML 요소 이름을 받고 내부적으로 해당 요소의 속성 타입을 찾아 바인딩해주는 함수이다.

이렇게 필요한 부분을 추상화해두면 필자가 아닌 다른 개발자는 ComponentPropsWithoutRef을 사용해야한다던가 Combine 타입을 사용할 때 타입 변수 TElementType으로 제한해야한다던가 하는 귀찮은 부분을 생각하지 않고도 as 프로퍼티를 쉽고 빠르게 추가할 수 있을 것이다.

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
// 전체 코드
export type Combine<T, K> = T & Omit<K, keyof T>;

export type CombineElementProps<T extends ElementType, K = unknown> = Combine<
K,
ComponentPropsWithoutRef<T>
>;

type OverridableProps<T extends ElementType, K = unknown> = {
as?: T;
} & CombineElementProps<T, K>;

type TextBaseProps = {
typography?: string;
};

type TextProps<T extends ElementType> = OverridableProps<T, TextBaseProps>;

function Text<T extends ElementType = "span">(
{ typography = "content", as, ...props }: TextProps<T>,
ref: Ref<any>
) {
const target = as ?? "span";
const Component = target;

return (
<Component
ref={ref}
// 대충 타이포그래피 클래스 렌더하는 로직
{...props}
/>
);
}

export default forwardRef(Text) as typeof Text;

참조

Evans Library Blog - “타입스크립트와 함께 컴포넌트 단계별로 추상화하기”

댓글 공유

createPortal 사용하여 접근성 높인 모달 구현하기

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
// Modal.tsx
import { useEffect } from "react";
import { createPortal } from "react-dom";

function Modal({ onClick }: { onClick: () => void }) {
const handleModalClose = (e: KeyboardEvent) => {
if (e.key !== "Escape") return;
onClick();
};
useEffect(() => {
document.body.style.cssText = `position: fixed; top: -${window.scrollY}px`;
return () => {
const scrollY = document.body.style.top;
document.body.style.cssText = `position: ""; top: "";`;
window.scrollTo(0, parseInt(scrollY || "0") * -1);
};
}, []);

useEffect(() => {
document.addEventListener("keyup", handleModalClose);
return () => document.removeEventListener("keyup", handleModalClose);
});
return createPortal(
<div>
<article role="dialog" aria-modal="true" className="Dialog">
<header className="Dialog__header">
<h2>React 포털로 연 다이얼로그(with 모달)</h2>
</header>
<div className="Dialog__body">
<p>여기가 React 앱 밖의 세상인가요?!</p>
</div>
<footer className="Dialog__footer">
<button
type="button"
className="closeButton"
aria-label="모달 다이얼로그 닫기"
title="모달 다이얼로그 닫기"
onClick={onClick}
>
<svg
width="24"
height="24"
xmlns="http://www.w3.org/2000/svg"
fill-rule="evenodd"
clip-rule="evenodd"
>
<path d="M12 11.293l10.293-10.293.707.707-10.293 10.293 10.293 10.293-.707.707-10.293-10.293-10.293 10.293-.707-.707 10.293-10.293-10.293-10.293.707-.707 10.293 10.293z" />
</svg>
</button>
</footer>
</article>
<div className="Dialog__dim" onClick={onClick}></div>
</div>,
document.getElementById("modal") as HTMLElement
);
}

export default Modal;
  • createPortal로 root 태그가 아닌 새로운 id가 modal인 태그에 modal을 생성하였다.
  • Esc 키를 눌러 모달을 끌 수 있게 하였다.
  • 모달이 열려있을 때, 모달 뒤의 배경이 스크롤되지 않도록 하였다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// App.tsx
import { useState } from "react";
import "./App.css";
import Modal from "./Modal";
import ModalBtn from "./ModalBtn";

function App() {
const [modalOpen, setModalOpen] = useState(false);

const handleModalOpen = () => setModalOpen(true);
const handleModalClose = () => setModalOpen(false);
return (
<div className="App">
<ModalBtn onClick={handleModalOpen} modalOpen={modalOpen} />
{modalOpen && <Modal onClick={handleModalClose} />}
</div>
);
}

export default App;
  • ModalBtn을 클릭하여 모달을 열 수 있게한다.
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
// ModalBtn.tsx
function ModalBtn({
onClick,
modalOpen,
}: {
onClick: () => void;
modalOpen: boolean;
}) {
return (
<div className="box">
<button
type="button"
className="openDialogButton"
aria-haspopup="dialog"
aria-label="모달 다이얼로그 열기"
title="모달 다이얼로그 열기"
onClick={onClick}
tabIndex={modalOpen ? -1 : 0}
>
<svg
width="24"
height="24"
xmlns="http://www.w3.org/2000/svg"
fill-rule="evenodd"
clip-rule="evenodd"
>
<path d="M14 4h-13v18h20v-11h1v12h-22v-20h14v1zm10 5h-1v-6.293l-11.646 11.647-.708-.708 11.647-11.646h-6.293v-1h8v8z" />
</svg>
</button>
</div>
);
}

export default ModalBtn;
  • ModalBtn에 모달 상태를 전달하여 모달이 열려있다면 모달 배경에 포커스가 가지 않도록 하였다.

배경 요소에 모든 focus 요소에 tabIndex 속성을 설정해줘야하는 번거로움이 있다.

댓글 공유

리액트에서 이미지 넣기

1. import 구문 사용하기

1
import Logo from "./assets/img/Logo.png";

컴포넌트에서 import 구문을 사용하여 이미지를 불러올 수 있다.

이렇게 불러온 이미지는 img 태그의 src 속성에 할당한다.

1
2
3
4
5
6
7
function App() {
return (
<div>
<img src={Logo} alt="Logo">
</div>
)
}

단, import는 React가 처리하지 않는다는 것을 명심해야한다. 이는 Webpack이나 vite같은 번들러들에 의해 처리된다.

2. public 폴더에 이미지 불러오기

1
2
3
public
|__images
|__logo.png

다음과 같이 public 폴더에 images를 담아두고 public 폴더를 절대경로로 이미지를 불러올 수 있다.

1
2
3
4
5
6
7
function App() {
return (
<div>
<img src="images/logo.png" alt="Logo">
</div>
)
}

댓글 공유

데이터와 디자인을 컴포넌트로 구성하기

1
2
3
4
5
6
7
8
[
{ "category": "과일", "price": 1920, "stocked": true, "name": "사과" },
{ "category": "과일", "price": 1640, "stocked": true, "name": "두리안" },
{ "category": "과일", "price": 2480, "stocked": false, "name": "아보카도" },
{ "category": "채소", "price": 1700, "stocked": true, "name": "시금치" },
{ "category": "채소", "price": 2800, "stocked": false, "name": "호박" },
{ "category": "채소", "price": 1330, "stocked": true, "name": "완도콩" }
]

design

위와 같이 데이터와 디자인 시안이 제시되었을 때, 컴포넌트를 어떻게 구성하는 것이 좋을지 생각해본다.

UI와 데이터는 주로 동일한 모양을 갖기 때문에 데이터 구조와 컴포넌트 구조가 자연스럽게 매핑된다.

그러므로 다음과 같이 컴포넌트를 구성할 수 있을 것이다.

component

1
2
3
4
5
FilterableProductTable
├── SearchBar
└── ProductTable
├── ProductCategoryRow
└── ProductRow

폴더 구조는 위와 같이 구성할 수 있을 것이다.

이상적인 컴포넌트 설계는 한가지만 책임하는 것이다. 만약 한가지 이상의 처리가 필요하다면 컴포넌트를 더 작은 컴포넌트로 나눠야한다.

React 정적 컴포넌트 구성

컴포넌트에 대한 구상이 끝났으므로 React로 앱을 빌드해보자.

이 때 가장 간단한 접근법은 사용자와의 상호작용은 고려하지 않고 데이터 모델에서 UI를 렌더링 하는 버전으로 작성하는 것이다.

데이터 모델을 렌더링하는 앱을 만들고자 한다면 props와 state를 사용하고 싶을 것이다. 하지만 현재 단계에서는 state를 사용하지 않는다.
왜냐하면 state는 사용자와 상호작용할 때 사용하기 때문에 아직 단계가 아니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import PRODUCTS from "../../api/products.json";

function FilterableProductTable() {
const products = [...PRODUCTS];

return (
<div className="filterableProductTable">
<SearchBar />
<ProductTable products={products} />
</div>
);
}

export default FilterableProductTable;

정적 빌드된 컴포넌트는 JSX만을 반환한다. 그리고 컴포넌트 트리 최상위 컴포넌트에서 데이터 모델을 하위 props로 전달하는 “단방향 데이터 흐름”을 구현한다.

컴포넌트 UI 대화형으로 만들기

UI를 대화형으로 만들려면 사용자가 기본 데이터를 변경할 수 있도록 해야한다. 이러한 상호작용이 필요할 때, state를 사용한다.

state는 앱이 기억하고 변경해야 할 데이터의 최소 집합이라고 생각할 수 있다.

이 때 상태를 구성하는 가장 중요한 원칙은 DRY(Don’t Repeat Yourself)이다. state는 최소한으로만 필요해야하며 나머지는 필요할 때 다시 고민한다.

그렇다면 위 컴포넌트에서는 state는 무엇이 될 수 있을까?

state와 props 구분하기

1. 시간이 지남에도 변경이 없다면?

=> 이는 state가 아니다. state는 사용자와 상호작용해야하는 요소이기 때문이다.

2. props를 통해 상위 컴포넌트로 부터 전달될 수 있다면?

=> 이는 state가 아니다.

3. 컴포넌트의 기존 state 또는 props를 기반으로 계산할 수 있다면?

=> 이는 state가 아니다.

그러므로 위 예시에서 state는 검색 텍스트 값과 체크박스 값이 될 수 있다.

앱 상태 관리 하기

자 이제 앱이 가지는 최소한의 state를 파악했다. 그러면 이 state를 변경하고 소유하는 컴포넌트는 누가 할 것인지를 정해줘야한다.

React는 단방향 데이터 흐름을 사용하기 때문에 상위 컴포넌트에서 하위 컴포넌트로 데이터가 전달된다.

컴포넌트 상태 관리 전략

  1. state를 기반으로 무언가를 렌더링하는 모든 컴포넌트 확인한다.
  2. 해당 컴포넌트와 가장 가까운 공통 상위 컴포넌트를 찾는다.

이제 state를 어디서 관리할지 정해줘야한다. 경우의 수는 3가지가 있다.

  • state를 공통 상위 컴포넌트에서 관리
  • state를 공통 상위 컴포넌트보다 상위에서 관리
  • state를 소유하는 것이 타당한 컴포넌트를 찾지 못한 경우, state를 유지하기 위한 새로운 컴포넌트 만들고 공통 상위 컴포넌트 보다 위 어딘가에 추가하여 관리

위 예시에서 검색 텍스트 값과 체크박스 값이 state라고 하였다.

컴포넌트 상태 관리 단계

  1. 상태를 사용하는 컴포넌트 식별
  • SearchBar - 검색 텍스트 state 관리
  • ProductTable - 체크박스 유무 state 관리
  1. 공통 상위 컴포넌트 찾기

SearchBar, ProductTable 의 공통 상위 컴포넌트는 FilterableProductTable 컴포넌트이다.

  1. state가 있는 위치 결정

공통 상위 컴포넌트인 FilterableProductTable 컴포넌트에서 관리한다.

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
import PRODUCTS from "../../api/products.json";
import SearchBar from "./SearchBar/SearchBar";
import ProductTable from "./ProductTable/ProductTable";
import { useState } from "react";

function FilterableProductTable() {
const products = [...PRODUCTS];

const [searchText, setSearchText] = useState("");
const [inStockOnly, setInStockOnly] = useState(false);

const handeChangeSearchText = (newSearchText: string) =>
setSearchText(newSearchText);

const handeChangeInStockOnly = (inStockOnlyState: boolean) =>
setInStockOnly(inStockOnlyState);

return (
<div className="filterableProductTable">
<SearchBar
searchText={searchText}
inStockOnly={inStockOnly}
onChangeSearchText={handeChangeSearchText}
onChangeInStockOnly={handeChangeInStockOnly}
/>
<ProductTable
products={products}
searchText={searchText}
inStockOnly={inStockOnly}
/>
</div>
);
}

export default FilterableProductTable;

해당 컴포넌트에서 state를 관리하고 props로 전달하는 것은 완료했으니 state를 관리하는 컴포넌트에서 state 변경함수까지 props 전달하여 사용자가 UI를 업데이트 할 수 있도록 구현해주면 완성이다.

댓글 공유

netlify 배포하기

🖱 1. 드래그 앤 드랍으로 배포하기

  1. npm run build 명령어를 실행하여 build 파일을 만든다.
  2. netlify 홈페이지에 가서 build 폴더를 드래그 드랍한다.

netlify사이트

배포 끝!

🔗 Cocktail project site Link

❗️React Router 사용하여 netlify 배포시 문제

React Router를 사용하여 netlify에 배포했을 때, root 경로이외의 직접적으로 경로에 접근할 때, Page Not Found가 뜨는 문제가 있다.

📌 문제 발생 원인!

해당 문제는 React Router는 클라이언트 사이드에서 라우팅을 다루기때문에, root 페이지가 아닌 경로로 직접적으로 방문했을 경우, netliffy는 해당 경로를 어떻게 다룰지 알 수 없다.

🙆🏻‍♂️ 어떻게 해결하는가?

netlify에서 _redirects 파일을 제공한다. 여기에 netlify가 클라이언트 사이드에서 다루지 않는 URL을 어떻게 다룰 지에 대한 코드를 적어주면 된다.

1
2
// _redirects 파일
/* /index.html 200

폴더구조

SPA 프레임워크를 사용하여 애플리케이션을 만들고 history pushstate를 사용하여 URL을 클린하게 사용하는 경우 위와 같이 파일을 생성하여 적어주면된다.

이렇게 하면 어떤 URL로 브라우저 요청이 오던지 간에 404 대신 index.html을 제공한다.

2. github에서 CI 사용하여 배포하기

이 방법을 사용하면 프로젝트 변경사항을 반영하여 배포하기 편리하다.

  1. 깃헙 레포를 생성한다.
  2. 이미 존재하는 파일을 깃헙 레포에 푸시한다.
1
2
3
4
5
6
git init
git add .
git commit -m "initial commit"
git remote add origin https://github.com/loco9939/react-cocktail-project.git
git branch -M main
git push -u origin main
  1. netlify에 가서 “add new site” 버튼 클릭한 후 github으로 배포하기 하여 생성한 레포를 선택하고 배포한다.

netlify사이트

  1. package.json에 가서 build 명령어를 수정해주자.
1
2
3
4
5
6
7
8
// package.json
{
"scripts": {
...
"build": "CI= react-scripts build",
...
}
}

보통 react-scripts build만 되어있을텐데 앞에 CI= 을 추가해주자.

  1. 이후 해당 개발 파일을 수정한다음 깃헙에 push 하게 되면 netlify 가 알아서 배포를 해준다.

body 태그의 배경 색깔을 변경해보았다.

색깔변경후화면

댓글 공유

loco9939

author.bio


author.job