티스토리 뷰
1) alias 적용
2) 중복코드 제거
3) 스니펫 만들기
4) useAuth 훅 만들기
5) react-query 적용
6) 다양한 UI 경험
1. alias 적용
하위 컴포넌트를 import 해오면서 상대경로로 적혀있었다.
새로운 폴더, 파일들을 만들면서 가장 최근에 만든 파일들을 보면 상대경로가 굉장히 복잡해졌다.
../../ 이런 식으로 상대경로의 뎁스가 깊어지고 있다. 이렇게 되면 점점 복잡성이 늘어진다면 import 관리도 힘들어진다.
그래서 상대경로들을 절대경로로 바꾸었다.
craco와 craco-alias 를 설치해서 사용했다.
그 다음, tsconfig.json 파일에 상대경로를 절대경로로 바꾸기 위한 패스에 대한 내용을 추가해주어야한다.
그래서 tsconfig.paths.json 파일을 새로 만들고 추가적인 내용을 여기에 적고, tsconfig.json 파일에서 extends 를 하기로했다.
tsconfig.json에 extends를 넣어서 새로만든 tsconfig.paths.json 을 적어주었다.
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
tsconfig.paths.json은 이렇게 적으면 된다.
그리고, craco.config.js 파일도 하나 만들어준다.
const cracoAlias = require("craco-alias");
module.exports = {
plugins: [
{
plugin: cracoAlias,
options: {
source: "tsconfig",
baseUrl: ".",
tsConfigPath: "tsconfig.paths.json",
debug: false,
}
}
]
};
그러고나서 tsconfig.json에서 include 부분에 "craco-config.js" 를 추가 해주어야한다.
마지막으로 package.json 파일에 있는 scripts 부분에서 start, build, test 부분에 적혀있는
react-scripts 를 지우고 craco로 변경해주면 된다.
그럼 npm run start 실행을 하면 craco start 와 같으니 실행 된다.
이렇게 이나 ../ 이나 ../../ 등 상대경로로 적힌 부분에 @ 넣어준 후 / 를 넣어주면 절대경로를 사용할 수 있다.
2. 중복코드 제거
대표적으로 App.tsx 에선 router 안에 반복적인 코드들이 있다. <Layout></Layout> 이다.
그리고 루트패스에 errorElement 라고 Error 컴포넌트를 전달하고 있는데,
이 컴포넌트는 루트에만 있기 때문에 루트 하위에 작성되지 않은 라우터에 대해서만 작동한다. 404 Not Found를 띄워준다.
이외에 react router 에서 제공해주는 에러 바운더리(error boundary)라는 기능도 있다.
이 기능을 하위의 모든 라우트에서 이용하고 싶다면?
이 element를 매번 전달해주어야 하는데 그럼 또 중복이 발생하기 때문에 이 부분도 리팩토링 해보려 한다.
import { 생략 }
const routeList = [
{
path: "/",
element: <Home />,
},
{
path: "/books",
element: <Books />,
},
{
path: "/signup",
element:<Signup />,
},
{
path: "/reset",
element: <ResetPassword />,
},
{
path: "/login",
element: <Login />,
},
{
path: "/books/:bookId",
element: <BookDetail />,
},
{
path: "/cart",
element: <Cart />,
},
{
path: "/order",
element: <Order />,
},
{
path: "/orderlist",
element: <OrderList />,
},
]
const router = createBrowserRouter(
routeList.map((item) => {
return {
...item,
element: <Layout>{item.element}</Layout>,
errorElement: <Error />
}
})
)
function App() {
return (
<BookStoreThemeProvider>
<RouterProvider router={router} />
</BookStoreThemeProvider>
)
}
export default App
우선, 전달하는 인자인 배열 [ ] 부분은 바꿀 수 있고, 바깥에 있는 createBrowserRouter 부분은 바꾸기 힘든 부분이다.
그래서 배열 부분은 바깥으로 빼내고, routeList 라는 변수에 담아주었다.
routeList 는 route 의 반복인 배열 [ ] 형태 이기 때문에 map 함수를 사용해서 배열을 순회하면서,
element의 <Layout></Layout> 을 전달하고, errorElement의 <Error /> 넣기로 했다.
map 은 배열을 순회하면서 새로운 배열을 return 하는 함수이다.
item 은 routeList 에서 정의한 하나하나의 라우터가 된다. route 를 순회하고 item 을 return 해준다.
그 다음, element 와 errorElement 를 추가해주었다.
에러 없이 화면이 잘 뿌려지는 걸 볼 수 있다.
map 함수를 잘 모르면 이해하기 어려운 부분일 수도 있지만, 중복된 코드가 많이 없어짐으로써 한 눈에 보기도 좋고 깔끔해졌다.
이번엔 최근에 만든 order api 에 대한 코드를 리팩토링 했다.
order api의 코드는 딱 봐도 중복코드가 많다. const response 부분부터 해서 return 까지 똑같다.
하지만 파라미터로 전달돼서 함수마다 다른 인자들이나 메소드(post, get)들은 다 다르기 때문에 공통화를 할 수가 없다.
그래서 바꿀 수 있는 것들만 즉, 공통화 할 수 있는 것들과 바꾸면 안 되는 것들을 분리해서 작업해야한다.
http 파일을 보면, 기존에는 공통화의 대상으로 axios instance 로 만들어진 httpClient 정도만 export 해서 작업했다.
이번엔 requestHandler 를 만들어서 추가적으로 공통화를 했다.
requestHandler 는 모든 request 들을 Handling 하겠다. 라는 의미이다.
requestHandler 는 async 를 받고, 첫 번째 인자로 메소드를 지정해주었다.
메소드는 string 타입으로 지정해줘도 되지만, "get", "post", "put", "delete" 외에는 올 수가 없다.
그래서 RequestMethod 라는 타입을 지정해서 4개로 제한해주었다.
이렇게 되면 order api 에 있는 const response ~ 부분을 const response = await httpClient[method] 로 공통화 시킬 수 있다.
두 번째 인자는 모든 요청에 url(엔드포인트)가 필요하기 때문에 url로 지정해주었다.
그럼 order api 에 있는 "/orders" 부분을 공통화 할 수 있다.
세 번째 인자는 payload 로 지정해주었다. http 요청에선 body 라고도 부른다.
하지만 payload 는 모든 요청이 같은 payload type 을 가질 수 없고 매번 다르다.
이럴 땐, 가변적인 타입인 제네릭(Generic)을 쓸 수 있다. 보통 제네릭의 타입은 <T> 로 표현한다.
이렇게 되면 payload 는 T 라는 타입이 전달되는 것에 따라 타입이 결정된다.
보통 "get", "delete" 메소드는 payload 가 없다. "delete" 메소드는 무조건 없는 건 아니다.
하지만 현재 프로젝트에선 "delete" 메소드는 payload 가 없다.
그리고 이걸 토대로 switch 문으로 분기처리했다. switch 문도 리팩토링 대상이긴 하나 일단은 switch 문으로 했다.
이렇게 추가된 requestHandler 를 order api 에 사용해보았다.
requestHandler 가 이미 response 를 return 하고 있기 때문에 return 뒤에 await 를 적어주면 된다.
이렇게 중복제거를 하면서 리팩토링을 하면 중복코드가 줄어들고 제네릭을 통해서 payload 의 타입도 별도로 지정이 가능하다.
3. 스니펫 만들기
스니펫에 대한 내용들은 너무 궁금했던 부분이라 중간 회고 전에 미리 들었다.
4. useAuth 훅 만들기
Auth 를 제외한 Book, Cart, Order 부분은 훅에 데이터 흐름을 위임하고 있다.
하지만 로그인, 회원가입 등은 훅을 사용하고 있지 않기 때문에 useAuth 를 만들었다.
import { 생략 }
export const useAuth = () => {
const navigate = useNavigate()
const { showAlert } = useAlert()
const { storeLogin, storeLogout, isloggedIn } = useAuthStore()
const userLogin = (data: LoginProps) => {
login(data).then((res) => {
storeLogin(res.token)
showAlert("로그인이 완료되었습니다.")
navigate("/")
}, (error) => {
showAlert("로그인이 실패했습니다.")
})
}
const userSignUp = (data: SignupProps) => {
signup(data).then((res) => {
showAlert("회원가입이 완료되었습니다.")
navigate("/login")
})
}
const userResetPassword = (data: SignupProps) => {
resetPassword(data).then(() => {
showAlert("비밀번호가 초기화되었습니다.")
navigate('/login')
})
}
const [resetRequested, setResetRequested] = useState(false)
const userResetRequest = (data: SignupProps) => {
resetRequest(data).then(() => {
setResetRequested(true)
})
}
return { userLogin, userSignUp, userResetPassword, userResetRequest, resetRequested }
}
Login 같은 경우엔 onSubmit 을 했을 때, onSubmit 은 리액트 훅 폼의 데이터를 전달해주는 역할을 한다.
그래서 api 요청을 바로 하기 때문에 promise 의 then, error 같은 부분들을 한꺼번에 처리해줘야한다.
이렇게 되면 컴포넌트에서 코드양이 늘어나고 컴포넌트에서 보여지지 않아야 할 부분들이 같이 보여지게 된다.
그래서 전체적으로 인증 부분을 관리해주는 useAuth 훅을 만들었다.
보통 상태를 갖고, 메소드를 작성하고, 리턴을 한다.
상태는 storeLogin, isloggedIn 등 이런 걸 useAuthStore 에서 관리했다.
그래서 별도의 state 상태를 만들지 않고 useAuthStore 에서 storeLogin, storeLogout, isloggedIn 을 가져왔다.
첫 번째 메소드는 userLogin 으로 만들었다.
여기서 받는 data 는 email, password 이다. 이에 해당하는 LoginProps 를 Login.tsx 에서 가져오고,
onSubmit 에 적었던 코드들을 가져왔다. 데이터가 딱 맞기 때문에 문제는 생기지 않는다.
그리고 userLogin 이라는 메소드는 return 에 적어준다. 그럼 이제 Login 에서 useAuth 의 userLogin 메소드를 쓸 수 있게 된다.
onSubmit 에 적혀있던 코드들은 useAuth 로 옮겼으니 지우고 userLogin 으로 대체하면된다.
그러면 아까보다 훨씬 간결하고 가독성 있는 코드로 바뀐다.
이렇게 똑같은 방법으로 Signup.tsx 도 리팩토링했다.
ResetPassword.tsx 역시 위의 2개와 다르지 않게 똑같은 방법으로 리팩토링했다.
하지만 ResetPassword.tsx 엔 ResetPassword 만 있는 게 아니라 ResetRequest 도 같이 있다.
ResetRequest 같은 경우엔 위의 3개와 다르게 콜백이 존재한다.
그래서 훅에서 콜백을 줘야하기 때문에 상태를 가져와서 state 도 useAuth 에서 관리하기로 했다.
가져온resetRequested 역시 return 해준 후, ResetPassword.tsx 에 불러와서 쓰면된다.
원래의 코드처럼 if 문을 사용해도 되지만 삼항식으로 표현하면 더 간결하고 짧기 때문에 가독성이 좋아서 삼항식을 선택했다.
이렇게 storeLogin 처럼 Header 에서 사용하는 storeLogout, isloggedIn 도 useAuth 로 옮겨서 관리해도 좋을 것 같다.
나중에 시간이 난다면 이 부분은 혼자서 해결해 볼 예정이다.
5. react-query 적용
react-query 는 굉장히 많이 사용되고 있는 라이브러리 이다.
기본적으로 자동으로 데이터를 동기화 해주는 장점이 있다.
useBooks 와 같은 훅에서 useState 와 useEffect 를 통해서 fetch 한 내용을 잘 업데이트 하고 있다.
하지만 react-query 를 사용하면 이런 부분들을 조금 더 날카롭게 관리를 해준다.
그리고 사용자가 화면을 터치, 클릭을 하거나 부가적인 기능도 지원을 한다.
해당 부분을 계속 fetch 를 하는 게 아니고, 일정 부분을 캐싱을 하거나 캐싱을 부여하는 것도 제공한다.
books, pagination 과 같은 선언적인 데이터, 로딩 상태, 에러 상태, 에러 핸들링을 기본적으로 제공한다.
useState 와 useEffect 로 나누던 것을 하나로 통합해서 관리할 수 있고,
코드의 길이도 짧아지고 생산성 향상, 일관적인 구조를 지킬 수 있어 데이터 흐름을 파악하는 데도 유용하다.
설치를 하고 가장 먼저 해야할 일은 쿼리 클라이언트를 작성해야한다.
쿼리 클라이언트는 이전에 만들었던 httpClient와 비슷하다.
new QueryClient( ) 는 간단히 생성자로서 인스턴스 생성만 했는데, 필요에 따라 옵션을 더 넣을 수 있다.
그리고 최상위 엔트리인 App.tsx 에 Provider 를 작성해야한다.
QueryClientProvider 를 컴포넌트 형식으로 전체를 감싸준다.
Provider에 queryClient.ts 에 만들었던 queryClient 를 import 해서 가져온다.
그럼 이 QueryClientProvider 가 관리하는 하위의 모든 컴포넌트 훅에서 react-query 를 사용할 수 있게 된다.
이 react-query 를 이용하여 useBooks 파일을 바꿨다.
useBooks 는 useState 와 useEffect 로 전체적인 데이터를 관리하고있다.
이걸 하나의 useQuery 로 관리할 수 있다. useQuery 는 선언적 데이터를 미리 받을 수 있다.
첫 번째 인자로 key 가 들어간다.
books 와 같이 string, string 배열로 넣을 수 있다.
react-query 는 캐싱을 하고 있기 때문에 books 라고만 처리를 해주면 쿼리스트링 파라미터가 업데이트 되는 동안
캐싱에 의해서 데이터가 업데이트 되지 않을 수 있다.
그렇기 때문에 쿼리스트링의 변화에 따라서 key 에 변화가 있을 때 refetch 를 하게 해줘야한다.
그래서 string 배열로 넣어주었고, books 뒤에는 동적인 id 같은 것을 넣을 수 있는데 location.search 를 넣어주었다.
location.search 는 쿼리스트링을 전체적으로 리턴하고 동작에 의해서 계속 바뀌고 있기 때문에 이렇게 해주면 key 가 동적으로 생성이 된다.
뒤에는 콜백(함수)가 들어가고 then 을 제외한 fetchBooks 를 그대로 넣어주었다.
이렇게되면 useQuery 가 이 부분을 관리하게되고 data 와 같이 선언적인 부분들을 계속 업데이트 해준다.
그렇기 때문에 이제부터 상태들도 필요가 없어지게 된다. useQuery 에서 선언하고있는 data 가 이미 상태이기 때문이다.
data 는 별칭으로 booksData 로 칭했다.
return 값도 useState 의 books, pagination 등을 가지고 왔었는데 이젠 booksData 를 가지고오면된다.
books 는 booksData 의 books 값
pagination 은 booksData 의 pagination 값
isEmpty 는 booksData 의 books 의 길이가 0인 값을 리턴한다.
undefined 가 될 수 있기 때문에 옵셔널(?) 도 붙여주었다.
또한, 리턴 된 값도 undefined 가 될 수 있기 때문에 값의 타입이 바뀌게 된다.
Books.tsx 에 가져온 값들이 undefined 일 수도 있기 때문에,
books와 pagination 을 추가하여 존재할 경우에만 컴포넌트를 렌더하게 변경하였다.
isLoading 같이 부가적인 상태도 잘 처리해준다. isLoading 은 그대로 리턴하기 보단 isBooksLoading 으로 별칭을 지어준 후,
isBooksLoading 도 리턴에 적어주었다.
그리고 Loading.tsx 파일을 새로 만들어서, 로딩중일 땐 로딩 아이콘이 360도로 빙글빙글 돌아가는 애니메이션 스타일을 주었다.
이렇게 아까보다 훨씬 간결하고 보기 좋게 코드가 변경되었다.
useState, useEffect 를 1개의 useQuery 로 처리할 수 있어서 import 도 깔끔해 보인다.
이제 리턴 한 데이터들을 Books.tsx 에서 받아서 처리해야한다.
이렇게 books, pagination, isEmpty, isBooksLoading 을 전부 가져와준 후,
얼리리턴을 사용해서 좀 더 간결하게 코드를 수정했다.
isEmpty 같이 렌더 조건문으로 복잡했던 코드들이 얼리리턴을 통해서 조건으로 리턴되니 굉장히 좋았다.
하지만 원래 isEmpty 일때 BooksEmpty 를 리턴하는 건 맞지만 BooksFilter 도 리턴했었는데,
이렇게 바꾸고 나니 BooksFilter 는 빠지고 BooksEmpty 만 나와서 좀 아쉬웠다.
그리고 books 와 pagination 역시, 정말 해당 데이터가 없는 걸 수도 있지만,
데이터의 전달오류 같은 경우도 있기 때문에 보통은 Loading 을 리턴하지않고 null 을 리턴 한다고 하셨다.
이 부분 역시 한 번 혼자 찾아서 해보는 것도 재밌을 것 같다.
이렇게 데이터를 받아오면서 걸리는 로딩 시간동안 저렇게 로딩 아이콘이 빙글빙글 돌아간다.
'웹 개발 공부하기' 카테고리의 다른 글
[02.11] 도서 상세 - 리뷰 (2) | 2025.02.17 |
---|---|
[02.11] 모킹 서버 (1) | 2025.02.17 |
[02.10] Book-Shop(Front) - 중간 회고 (0) | 2025.02.16 |
[02.10] 스니펫(Snippet) (0) | 2025.02.15 |
[02.07] 장바구니, 주문서 화면 구현해보기🤗 (2) | 2025.02.15 |