๋๋ฃํ์ต์ ํตํด์ ํ์์ ์๊ฐํ ์ํฐ๋ ํ๋ฆฌ์จ๋ณด๋ฉ ํ๋ก ํธ์๋ ์ธํด์ญ ์ ๋ฐ ๊ณผ์ ์ Best Pratice๋ฅผ ๋ง๋ค๊ณ ์ ์ถํด์ฃผ์ธ์.
Best Practice๋ ๋ชจ๋ฒ์ฌ๋ก๋ผ๋ ๋ง๋ก์, ํน์ ๋ฌธ์ ๋ฅผ ํจ๊ณผ์ ์ผ๋ก ํด๊ฒฐํ๊ธฐ ์ํ ๊ฐ์ฅ ์ฑ๊ณต์ ์ธ ํด๊ฒฐ์ฑ ๋๋ ๋ฐฉ๋ฒ๋ก ์ ์๋ฏธํฉ๋๋ค.
์งํ ๊ธฐ๊ฐ: 2023.06.27 ~ 2023.06.30
- ๐ ๋ฐฐํฌ ๋งํฌ
- โ๏ธ ์คํ ๋ฐฉ๋ฒ
- ๐ ์ฌ์ ์ ๋ฐ ๊ณผ์
- ๐ค ํ ๊ท์น
- ๐ ํด๋ ๊ตฌ์กฐ
- ๐ ๏ธ ๊ธฐ์ ์คํ
- ๐ ์๋น์ค ์๊ฐ
- ๐ Best Practice
https://todolist-1-1.vercel.app/
$ npm install
$ npm start
์ด๋ฆ | GitHub Repository |
---|---|
๊ถ๋ฒ์ค | @kjungit |
๊นํ์ง | @who0803 |
๋ฐํฌ์ง | @hihijin |
์์๋ฏผ | @sonmansu |
์ ์งํ | @yoojiih |
์ ์๋ฒ | @Boom0704 |
ํ๊ฐํฌ | @khkh0109 |
- upstream์๋ main ๋ธ๋์น๋ง ์กด์ฌ
- ๋ธ๋์น๋ช
:
feature/#์ด์๋ฒํธ-๊ฐ๋จํ์ค๋ช
- ex:
feature/#7-setting
- ex:
- fork ํด์ ๋ธ๋์นํ์ ์์
ํ๋ค
origin:main
์ผ๋ก PR ๋ ๋ฆผ - ์ฝ๋๋ฆฌ๋ทฐ ๋ฐ๊ณ ์น์ธ ๋ฐ์ผ๋ฉด
origin:main
์ merge
ํ์ | ์ค๋ช |
---|---|
Feat | ์๋ก์ด ๊ธฐ๋ฅ ์ถ๊ฐ |
Fix | ๋ฒ๊ทธ ์์ |
Env | ๊ฐ๋ฐ ํ๊ฒฝ ๊ด๋ จ |
Style | ์ฝ๋ ์คํ์ผ ์์ (์ธ๋ฏธ ์ฝ๋ก , ์ธ๋ดํธ ๋ฑ์ ์คํ์ผ์ ์ธ ๋ถ๋ถ๋ง) |
Refactor | ์ฝ๋ ๋ฆฌํฉํ ๋ง (๋ ํจ์จ์ ์ธ ์ฝ๋๋ก ๋ณ๊ฒฝ ๋ฑ) |
Design | CSS ๋ฑ ๋์์ธ ์ถ๊ฐ/์์ iE |
Comment | ์ฃผ์ ์ถ๊ฐ/์์ |
Docs | ๋ด๋ถ ๋ฌธ์ ์ถ๊ฐ/์์ |
Test | ํ ์คํธ ์ถ๊ฐ/์์ |
Chore | ๋น๋, ํจํค์ง ๊ด๋ จ ์ฝ๋ ์์ |
Rename | ํ์ผ ๋ฐ ํด๋๋ช ์์ |
Remove | ํ์ผ ์ญ์ |
๐ฆsrc
โโโ ๐index.css
โโโ ๐index.tsx
โโโ ๐App.tsx
โโโ ๐components
โโโ ๐hooks
โโโ ๐pages
โโโ ๐routers
โโโ ๐types
โโโ ๐utils
- ํ์ ๊ฐ์
- ๋ก๊ทธ์ธ
- Todo CRUD
- ํ ํฐ ์ ๋ฌด์ ๋ฐ๋ฅธ ํ์ด์ง ๋ฆฌ๋ค์ด๋ ์
ํ์๊ฐ์ |
---|
๋ก๊ทธ์ธ |
---|
Todo |
---|
- ์ฝ๋์ ๊ฐ๋ ์ฑ ๋ฐ ์ฌ์ฌ์ฉ์ฑ
- ๊ฐ๋ ์ฑ์ ํ์ ์ ํ๋ฉด์๋ ์ค์ํ๋ฉฐ, ์ ์ง ๋ณด์์ฑ์ ํฅ์
- ์ค๋ณต ์ฝ๋๋ฅผ ์ต์ํ ํ๊ณ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ์ ํธ๋ฆฌํฐ ํจ์ ์์ฑ(useHook)
- ํ์ฅ์ฑ
- ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ธฐ๋ฅ์ ์ ์ฐํ๊ฒ ํ์ฅํ๊ณ ์ ์ง ๊ด๋ฆฌํ ์ ์๋๋ก ์์ฑ
- ์ฌ์ฉ์ ํธ์์ฑ
- ์ฌ์ฉ์๋ค์ด ์น ๋๋ ์ฑ ์ธํฐํ์ด์ค๋ฅผ ์ฝ๊ฒ ์ดํดํ๊ณ ์กฐ์ํ ์ ์๋๋ก ์ค๊ณ
- Best Practice๋ฅผ ์ ์ ํ๊ธฐ์ ๊ฐ์ ๊ตฌํํ ์ฝ๋๋ฅผ ์ค๋ช ํ๋ฉฐ ์ฝ๋๋ฆฌ๋ทฐ๋ฅผ ์งํ
- ๊ฐ์ ๋งก์ ์ฝ๋ ๊ตฌํ ๋ฐฉ๋ฒ๋ค์ ์ ๋ฆฌ ํ ์คํฌ๋ผ์ ํตํด Best Practice ์ ์
- ์ฝ๋์ปจ๋ฒค์ ๋ฐ ์ธ์ด, ๋ผ์ด๋ธ๋ฌ๋ฆฌ, ํด๋๊ตฌ์กฐ ์ ๋ฆฌ
- Best Practice๋ก ๋ฝํ ๊ตฌํ๋ฐฉ์์ค ๊ตฌํํ์ง ์์ ๋ถ๋ถ๋ค์ ๋๋์ด ์ ๋ฌด ๋ถ๋ด
- ์ด์ ์์ฑ ๋ฐ ๊ฐ๋ณ์ ์ผ๋ก ์ฝ๋ ๊ตฌํ ํ pr ํ ์ฝ๋๋ฆฌ๋ทฐ๋ฅผ ์งํ ํ๊ณ merge
- Todo ๊ด๋ จ ๋ก์ง์ ์ปค์คํ ํ ์ผ๋ก ๋ถ๋ฆฌ
- ๊ธฐ์กด TodoPage์ TodoList์ ํผ์ฌ๋ผ์๋ Todo ๊ด๋ จ ๋ก์ง์
useTodo
๋ผ๋ ์ปค์คํ ํ ์ผ๋ก ๋ถ๋ฆฌ์ํด. ์ด๋ฌํ ๋ถ๋ฆฌ๋ ๋ค์์ ์ด์ ์ ๊ฐ์ง๋ค๊ณ ํ๋จํจ- Todo ๊ด๋ จ ๋ก์ง์
useTodo
ํ ์ ์ํด ๊ด๋ฆฌ๋๋ฉฐ, ์ด๋ก ์ธํด ์์ง๋ ฅ๊ณผ ์ ์ง๋ณด์์ฑ์ด ํฅ์๋จ - ์ถํ์ Todo๋ฅผ ์ ์ฅํ๋ ๋ฐฉ์์ด ๋ณ๊ฒฝ๋๋ค๋ฉด
useTodo
ํ ๋ด์์๋ง ์์ ํ๋ฉด ๋จ - TodoPage ์ปดํฌ๋ํธ์์ ๋น์ฆ๋์ค ๋ก์ง์ ๋ถ๋ฆฌํจ์ผ๋ก์จ, TodoPage ์ปดํฌ๋ํธ์ ์ฝ๋๊ฐ ๊ฐ๊ฒฐํด์ง๊ณ ๊ด์ฌ์ฌ๊ฐ ๋ถ๋ฆฌ๋จ
- Todo ๊ด๋ จ ๋ก์ง์
- Todo ์์ดํ ์ ์์ ์ํ๋ฅผ state ๋ณ์๋ก ๊ด๋ฆฌ
-
ํฌ๋ ์์ดํ ์ ์์ ์ํ๋ฅผ TodoItem ๋ด์ ์ํ ๋ณ์๋ก ๊ด๋ฆฌํ ์ง, Todo ๊ฐ์ฒด์ ์์ฑ์ผ๋ก ๊ด๋ฆฌํ ์ง์ ๋ํ ๋ ผ์ ํ์ ์ํ ๋ณ์๋ก ๊ฒฐ์
- TodoItem ์ปดํฌ๋ํธ ๋ด์ ์ํ ๋ณ์๋ก ์์ ์ํ ๊ด๋ฆฌ
export default function TodoItem({id, todo, isCompleted, updateTodo, deleteTodo }: TodoItemProps) { const [isOnModify, setIsOnModify] = useState(false);
- TOdo ๊ฐ์ฒด์ ์์ฑ์ผ๋ก ์์ ์ํ๋ฅผ ๊ด๋ฆฌ
export interface Todo { id: number; isCompleted: boolean; todo: string; isOnModify: boolean; // ์ถ๊ฐ }
ํ์์ ์ ๊ทผ ๋ฐฉ์์ Todo๋ฅผ ์์ ํ ๋๋ง๋ค ์ ํ๋ Todo์ ํด๋นํ๋ Todo๋ฅผ Todo ๋ฐฐ์ด์์ ๊ฒ์ํด์ผ ํ๋ ๋นํจ์จ์ด ์กด์ฌํจ. ๋ํ, Todo ๋ฐฐ์ด์ ๊ด๋ฆฌํ๋ ๋ถ๋ชจ (TodoPage) ์ปดํฌ๋ํธ๋ถํฐ ๋ฆฌ๋๋๋ง์ด ๋์ด์ผ ํ๋ค๋ ๋จ์ ์ด ์กด์ฌํจ
๋ฐ๋ฉด, ์ ์์ ์ ๊ทผ ๋ฐฉ์์ ๋ถํ์ํ ๋ฐ๋ณต๋ฌธ์ ๋๋ฆฌ์ง ์์๋ ๋๊ณ ์์ (TodoItem) ์ปดํฌ๋ํธ๋ง ๋ฆฌ๋๋๋ง๋จ
- Todo ์์ ์ setTodo ํธ์ถ๋ก ๋ฆฌ๋๋๋งํ๊ธฐ
-
Todo๋ฅผ ์์ ํ ํ, ๋ณ๊ฒฝ๋ Todo๋ฅผ ํ๋ฉด์ ํ์ํ๊ธฐ ์ํด ๋ ๊ฐ์ง ๋ฐฉ๋ฒ์ด ์กด์ฌํจ
getTodos
API๋ฅผ ํธ์ถํ์ฌ ์ ์ฒด Todo ๋ฐฐ์ด์ ์ ๋ฐ์ดํธํ๋ ๋ฐฉ๋ฒsetTodos
๋ง ํธ์ถํ๋ ๋ฐฉ๋ฒ
Todo ์์ ํ ๋
getTodos
API๋ฅผ ๋ค์ ํธ์ถํ๋ฉด ์ ์ฅ ์ ๊น๋นก์ ํ์์ด ๋ํ๋จ update api ํธ์ถ์ด ์ฑ๊ณตํ๋ค๋ฉด, todo๊ฐ ์ ์์ ์ผ๋ก ์ ๋ฐ์ดํธ๋์๋จ ๊ฒ์ด ๋๋ฌธ์getTodos
api๋ฅผ ๋ค์ ํธ์ถํ ํ์๊ฐ ์๋ค๊ณ ํ๋จ๋์ดsetTodos
๋ง ํธ์ถํ๊ธฐ๋ก ํจ ์ด ๋ฐฉ์์ ๋ถํ์ํ ๋คํธ์ํฌ ์์ฒญ๊ณผ ๊น๋นก์ ๋ฌธ์ ๋ฅผ ์์จ ์ ์์
- input values๋ฅผ ์ ๋ ฅ ๋ฐ์ error message์ isDiabled ์์๋ฅผ ๋ฆฌํดํด์ฃผ๋ ์ ํธ ํจ์
- ์ ํจ์ฑ ๊ฒ์ฌ ๋ก์ง์ ๋งค๊ฐ๋ณ์๋ก ๋ถ๋ฆฌํ์ฌ ์ ํธ ํจ์๋ฅผ ์ฌ์ฉํ๋ ๊ณณ์์ ์ ํจ์ฑ ๊ฒ์ฌ ๋ก์ง์ ์์ฑํ ์ ์๋๋ก ํ์ฅ์ฑ ์๊ฒ ๊ตฌํ
const validateInput = ({ authInput, validate }) => {
const error = validate(authInput);
const isValid = () => {
return (
Object.values(error).every((err) => err === '') &&
Object.values(authInput).every((val) => val !== '')
);
};
return {
error,
isDisabled: !isValid(),
};
};
- localStorage key๊ฐ ๋ณ๊ฒฝ๋ ์ ์์ผ๋ ํจ์๋ก ๊ด๋ฆฌํ๋๊ฒ ์ ์ง๋ณด์ ์ธก๋ฉด์์ ํจ๊ณผ์ ์ผ ๊ฒ์ด๋ผ๊ณ ํ๋จ
- get, remove์ key๋ฅผ ์ ๋ฌํ์ง ์์๋ ํจ์๋ง ์ฌ์ฉํ๋ฉด ๋ฐ๋ก ํ ํฐ์ ๋ค๋ฃฐ ์ ์๊ฒ ๋ณ๊ฒฝ
export const getAccessToken = (): string | null => {
const accessToken = localStorage.getItem('accessToken');
return accessToken;
};
export const setAccessToken = (accessToken: string) => {
localStorage.setItem('accessToken', accessToken);
};
export const removeAccessToken = () => {
localStorage.removeItem('accessToken');
};
- ๋ ผ์: ํ ํฐ์ด ํ์ํ ์์ฒญ๊ณผ ํ์์๋ ์์ฒญ์ ์ธ์คํด์ค๋ฅผ ๋ถ๋ฆฌ vs ํ๊ฐ์ง ์ธ์คํด์ค๋ง ๋๊ณ ํ ํฐ ์ ๋ฌด์๋ฐ๋ผ ๋ค๋ฅด๊ฒ ์ฒ๋ฆฌ
- ๋ฌธ์ ์ : axios ์ธ์คํด์ค๊ฐ ๊ตณ์ด 2๊ฐ๋ผ ์ฝ๋๊ฐ ๊ธธ์ด์ก๊ณ , ๋ค๋ฅธ ์ปดํฌ๋ํธ์์ ์ธ ๋ ํ ํฐ์ด ํ์ํ ์์ฒญ์ธ์ง ์๋ ์์ฒญ์ธ์ง ๊ฐ์ ๊ตฌ๋ถํด์ ์จ์ผํด์ ๋ฒ๊ฑฐ๋ก์
//ํ ํฐ์ด ํ์ํ ์์ฒญ๊ณผ ํ์์๋ ์์ฒญ์ ์ธ์คํด์ค๋ฅผ ๋ถ๋ฆฌ
//๋ค๋ฅธ ์ปดํฌ๋ํธ์์ ํ ํฐ์ด ํ์ํ ์์ฒญ์ด๋ฉด tokenRequest ์ฌ์ฉ
//ํ ํฐ์ด ํ์์๋ ์์ฒญ์ด๋ฉด nonTokenRequest ์ฌ์ฉ
const BASE_URL = process.env.REACT_APP_BASE_URL;
const nonTokenRequest = axios.create({ baseURL: BASE_URL });
const tokenRequest = axios.create({ baseURL: BASE_URL }
{
headers: {
"Content-Type": "application/json",
},
}
);
)
tokenRequest.interceptors.request.use(config => {
config.headers = {
Authorization: `Bearer ${localStorage.getItem("access_token")}`,
};
return config;
});
- ํด๊ฒฐ๋ฒ:
- axios ์ธ์คํด์ค๋ฅผ ์ ์ด์ ํ๋๋ง ๋๊ณ localstorage์ ํ ํฐ์ด ์๋ค๋ฉด ์ด์ ๋ ํ ํฐ์ด ํ์ํ ์์ฒญ๋ฐ์ ์์ผ๋ฏ๋ก localstorage์ ํ ํฐ ์ ๋ฌด๋ง ํ์ธ
- ๋ํ localstorage์ ํ ํฐ์ ๋ถ๋ฌ์ฌ ๋ ๋ง์ฝ ํ ํฐ์ด ์๋ค๋ฉด null๋ก ๋ฐ์์ง๋ฏ๋ก ํค๋์๋ ์๋ฌด๊ฒ๋ ๋ด๊ธฐ์ง ์๊ฒ ๊ตฌํ
//ํ๊ฐ์ง ์ธ์คํด์ค๋ง ๋๊ณ ํ ํฐ ์ ๋ฌด์๋ฐ๋ผ ๋ค๋ฅด๊ฒ ์ฒ๋ฆฌ
export const API = axios.create({
baseURL: 'https://www.pre-onboarding-selection-task.shop',
headers: {
'Content-Type': 'application/json',
},
});
API.interceptors.request.use(
(config) => {
const accessToken = getAccessToken();
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
},
(error) => {
return Promise.reject(error);
},
);
- ๋ ผ์: ํด๋์ค ๋ฌธ๋ฒ์ ์ฌ์ฉํ ์ ์ ๋ฉ์๋vs ๊ฐ์ฒด์ ๋ฉ์๋
- ๋ฌธ์ ์ : ํ๋์ฝ๋ฉ๋ ์์ฒญ URL, ์ฝ๋์ ์ค๋ณต, ์ฝ๋์ ๊ตฌ์กฐํ๋ถ์ฌ๋ก ๊ฐ๋ ์ฑ ์ ํด
export const TodoAPI = {
async get(): Promise<ITodoResponse[]> {
const { data } = await API.get('/todos');
return data;
}
async post(todo: string): Promise<ITodoResponse> {
const { data } = await API.post('/todos', todo);
return data;
}
async put(todo: ITodo, id: number): Promise<ITodoResponse> {
const { data } = await API.put(`/todos/${id}`, todo);
return data;
}
async delete(id: number): Promise<string> {
const { data } = await API.delete(`/todos/${id}`);
return data;
}
}
- ํด๊ฒฐ๋ฒ:
- ํด๋์ค ๊ตฌ์กฐ์ ๊ฐ์ฒด์งํฅ์ ์ธ ์ ๊ทผ์ผ๋ก ์ฝ๋์ ๊ตฌ์กฐํ์ ๋ชจ๋ํ์์ผ ์ฝ๋ ๊ฐ๋ ์ฑ ํฅ์
- private ๋ฉค๋ฒ๋ฅผ ์ฌ์ฉํ์ฌ ์ ๋ณด ์๋๊ณผ ์บก์ํ, ๊ณตํต ์์ฒญ URL์ ๊ด๋ฆฌํ์ฌ ์ฝ๋์ ์ฌ์ฌ์ฉ์ฑ๊ณผ ์ค๋ณต ์ ๊ฑฐํ๊ณ ์ ์ง๋ณด์์ฑ์ ํฅ์
export class TodoAPI {
private static TODOS = '/todos';
static async get(): Promise<ITodoResponse[]> {
const { data } = await API.get(this.TODOS);
return data;
}
static async post(todo: string): Promise<ITodoResponse> {
const { data } = await API.post(this.TODOS, todo);
return data;
}
static async put(todo: ITodo, id: number): Promise<ITodoResponse> {
const { data } = await API.put(`${this.TODOS}/${id}`, todo);
return data;
}
static async delete(id: number): Promise<string> {
const { data } = await API.delete(`${this.TODOS}/${id}`);
return data;
}
}
- ๊ธฐ์กด์ ๋ผ์ฐํ ๊ธฐ๋ฅ๋ณด๋ค ๋ง์ ๊ธฐ๋ฅ๋ค์ด ์ถ๊ฐ๋์ด ์์ด ํ์ฉ์ฑ์ด ๋์
- loader, action ๋ฑ์ ํตํด์ ํ์ ์ปดํฌ๋ํธ๋ก ๋ฐ์ดํฐ๋ฅผ ์ ๋ฌ ๊ฐ๋ฅ
- ๊ฒฝ๋ก๊ฐ ๋ง๋ค๋ฉด ๊ฐ๋ ์ฑ์ด ์ข์
export const router = createBrowserRouter([
{
path: '/',
element: <Root />,
children: [
{
index: true,
element: <Navigate to='/todo' />,
},
{
path: 'signup',
element: <JoinPage />,
loader: redirectTodo,
},
{
path: 'signin',
element: <LoginPage />,
loader: redirectTodo,
},
{
path: 'todo',
element: <TodoPage />,
loader: redirectLogin,
},
],
errorElement: <NotFoundPage />,
},
]);
- ๋ผ์ฐํ ํ path, element๋ฅผ ์์ฑํ์ฌ ๋ผ์ฐํ ์ค์
- children์์๋ ๋ฐฐ์ด์ ์ค์ฒจ๋ ๋ผ์ฐํฐ๋ฅผ ์ถ๊ฐํ์ฌ ์ฌ์ฉ
- ํ์ฌ todo ์์๋ โ/โ ๋ฉ์ธ ํ์ด์ง๋ฅผ ์ฌ์ฉํ์ง ์๊ธฐ์ index๊ฐ์ true๋ก ์ค์ ํ์ฌ โ/โ ์ ๊ทผ์ โ/todoโํ์ด์ง๋ก ์ ํ๋๋๋ก ์ค์
export function Root() {
return (
<>
<Header />
<Outlet />
<Footer />
</>
);
}
- root๊ฒฝ๋ก์์ Header, Footer ์ค์
export const redirectPage = () => {
const token = getAccessToken();
if (token === null) {
return redirect('/signin');
} else {
return redirect('/todo');
}
};
export const redirectLogin = () => {
const token = getAccessToken();
if (token === null) {
return redirect('/signin');
}
return null;
};
export const redirectTodo = () => {
const token = getAccessToken();
if (token) {
return redirect('/todo');
}
return null;
};
- createBrowserRouter์์ loader๋ก ํ์ด์ง ์ ๊ทผ์ tokenํ์ธ ํ redirect ์ฒ๋ฆฌ๋๋๋ก ์ค์