항해 플러스에서 신규 기수(백엔드 8기, 프론트엔드 5기, AI 3기, 백엔드 1:1 커리어 코칭)를 모집한다고 합니다.
jg0hs4
생각 있으신 분들은 위 추천 코드를 입력하면 20만원 할인 받을 수 있으니 활용하세요!
항해 플러스를 진행하며, 클린 코드를 배우고 있다.
클린 코드는 단순히 잘 짠 코드를 넘어, 동료에 대한 배려, 유지 보수성, 테스트 용이성 등을 높여준다.
최적화가 필요한 상황이 오지 않도록 해주는 것도 클린 코드다.
클린 코드를 작성함으로써 오는 이점들이 많기 때문에, 클린 코드 마지막 과제를 시작하기 전에 발제 내용들을 정리해야겠다는 생각이 들었다.
발제 자료들을 정리하며 나름의 체크 리스트를 만들어 봤다.
과제를 하면서 틈틈히 확인해보려고 했지만 들춰보지 않았다.
운동을 결심하고 샀지만 옷걸이로 전략한 운동 기구랄까?
모니터에 붙여놔야 보려나...
지난 과제를 하며 추상화가 중요하다는 생각을 했다.
함수의 공통된 부분을 추출할 때도 그렇지만, 특히 리액트 경험이 많지 않은 나에게는 컴포넌트 추상화가 어려웠다.
해당 문제를 멘토링 과정 중에 멘토님께 여쭤봤다.
멘토님께서는 디자인 시스템의 코드를 보면 도움이 될 거라고 하셨다.
가장 핫한 디자인 시스템인 shadcn을 뜯어보기로 했다.
import { cva, VariantProps } from "class-variance-authority"
import * as React from "react"
import { forwardRef } from "react"
import { cn } from "../../lib/utils.ts"
const buttonVariants = cva(
"bg-blue-500 p-2", // 기본 클래스
{
variants: {
size: {
sm: "p-1",
lg: "p-4"
}
}
}
)
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
className?: string
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(({ className, variant, size, ...props }, ref) => {
return <button className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
})
Button.displayName = "Button"
shadcn의 Button
컴포넌트를 참고해서, 사용하지 않는 부분을 제외하고 className
을 간략하게 한 예시 코드다.
buttonVariants
는 cva
의 반환값이다.
cva
는 일종의 클래스 네임 파서라고 보면된다.
첫 번째 인자로 공통으로 설정할 클래스들을 선언하고, 두 번째 인자로 속성에 따라 파싱할 클래스를 작성해준다.
이때 defaultVariants
로 기본값을 정해줄 수도 있다.
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
cn
함수는 실제로 shadcn에서 구현되어 있는 유틸 함수다.
buttonVariants
에서 지정한 기본 클래스와 사용자가 className
으로 전달한 클래스가 충돌할 경우 이를 해결해준다.
자세히 살펴보면,
<Button size="sm" className="p-6 bg-red-500" />
위와 같이 컴포넌트를 사용했다면 먼저 buttonVariants
가 실행된다.
buttonVariants({ size: "sm", className: "p-6 bg-red-500" })
// 결과: "bg-blue-500 p-2 p-1 p-6 bg-red-500"
두 번째로 clsx
가 결과를 받아서 클래스들을 문자열로 합치고 조건부 클래스가 있다면 처리해준다.
clsx("bg-blue-500 p-2 p-1 p-6 bg-red-500")
// 결과: "bg-blue-500 p-2 p-1 p-6 bg-red-500"
마지막으로 twMerge
에서 같은 속성의 클래스들을 찾아서 충돌을 해결한다.
이때 우선 순위는 나중에 나온 값이 된다.
twMerge("bg-blue-500 p-2 p-1 p-6 bg-red-500")
// 결과: "p-6 bg-red-500"
// p-6이 p-2와 p-1을 덮어씀
// bg-red-500이 bg-blue-500을 덮어씀
forwardRef
로 감싼 이유는 부모 컴포넌트로부터 ref
를 props
로 받기 위해서다.
React19 버전부터는 forwardRef
로 감싸지 않아도 된다.
.display
는 ReactDevTools
에서 컴포넌트를 식별하기 위해 사용된다.
forwardRef
로 감싸면 컴포넌트 이름이 사라지기 때문에, 디버깅할 때 어떤 컴포넌트인지 알기 어려워진다.
이를 해결하기 위해 displayName
을 설정하면 개발 도구에서 Button
이라고 명확하게 표시된다.
설정하지 않으면 ForwardRef
로만 보여서 어떤 컴포넌트인지 파악하기 어렵다.
이번 과제는 리액트 쿼리와 전역 상태 관리 라이브러리를 활용한다.
전역 상태 관리 라이브러리는 대표적으로 Zustand
와 Jotai
가 있다.
둘 다 사용해본 적이 없어서 어떤 게 더 좋을까 찾아봤다.
Zustand와 Jotai에 대해 비교한 글(https://velog.io/@rinm/Jotai-Zustand).
여담으로 Zustand
와 Jotai
는 모두 같은 사람이 만들었다고 한다.
Jotai
는 리액트에 특화되어 있다고 한다.
Atom
이라는 개념을 사용하는 걸로 봐서는 Recoil
과 유사하다고 생각했다.
Zustand
는 기존에 Redux
를 써본 적 있다면 익숙할 거라고 했다.
예전에 Redux
강의를 들은 적이 있다.
내용이 어려워 대부분 까먹었지만...
그리고 Recoil
은 짧은 프로젝트에서 찍먹으로 써본 적이 있다.
그때 Redux
처럼 단일 객체에서 관리하지 않아서 헷갈렸던 기억이 있다.
팀원분께서 보라고 적극 추천하신 우아콘 영상(https://www.youtube.com/watch?v=nkXIpGjVxWU&t=168s)
팀원 분들이 대부분 현업에서 Zustand
를 사용하셔서 모르는 게 있을 때 물어보기 수월할 것 같아서 Zustand
로 결정했다.
위 영상은 같은 팀원 분이 추천해주신 영상이다.
최근 동향을 들어보면, 서버 상태는 리액트 쿼리, 클라이언트 전역 상태는 주스탠드 또는 조타이가 국룰인 것 같다.
리액트 쿼리는 22년 말에 Tanstack Query로 이름이 바뀌었다.
하지만 보통 리액트 쿼리라고 부르는 것 같다.
리액트 쿼리는 서버 데이터를 캐싱한다.
조회 시에 useQuery
, 업데이트 시에 useMutation
훅을 사용한다.
export const useTagsQuery = () => {
return useQuery({
queryKey: postKeys.tags().queryKey,
queryFn: () => fetchTags(),
})
}
먼저 내가 사용한 useQuery
의 예시다.
useQuery
에 인자로 들어가는 객체에 queryKey
로 고유 키를 넣어줘야 한다.
그러는 이유는 추후에 useMutation
으로 서버 데이터가 변경됐을 때 다시 데이터를 불러와 캐싱해야 하는데, 어떤 쿼리를 다시 실행할 건지를 구분하기 위해서다.
queryKey
에 직접 문자열의 key
를 입력해도 되지만, 서버 상태가 많아지면 일일히 기억하기 힘들어진다.
이를 해결하고자 나온 라이브러리가 @lukemorales/query-key-factory
이다.
이름에서 알 수 있듯이 queryKey
를 만들어준다.
export const postKeys = createQueryKeys("posts", {
fetch: (props: SearchParams & { searchQuery: string; tag: string }) => [props],
tags: () => ["tags"],
})
사용법은 위와 같다.
두 번째 인자로 넘어주는 객체의 key
에 원하는 이름을 입력하고 값에 queryKey
에 넣어줄 구분값?들을 넘겨준다.
다시 useQuery
로 돌아와서 queryFn
에 들어가는 fetchTags
는 서버로 부터 모든 태그들을 받아온다.
즉, 캐싱할 데이터를 불러오는 함수를 넣어주면 된다.
export const useAddPostMutation = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (newPost: RequestPost) => addPost(newPost),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: postKeys.fetch._def,
exact: true,
})
},
})
}
useMutation
을 사용한 코드다.
addPost
로 게시글을 등록하고 나면 onSccess
를 통해서 queryKey
로 넣어준 키들을 갖고 있는 useQuery
를 실행시켜 캐싱 데이터를 최신화한다.
exact
옵션은 true
일 경우 queryKey
로 넣어준 키값과 완전히 동일한 쿼리만, false
는 포함하고 있는 모든 쿼리를 실행시킨다.
이후에 추가적인 작업이 필요할 경우 invalidateQueries
앞에 await
키워드를 사용하여 .then
체이닝으로 처리할 수 있다.
보편적인 방법이 어떤지는 다른 사람들이 사용하는 걸 보지 못해서 팀원 분과 AI를 활용해 위와 같이 작성했다.
이번에 다른 분들의 코드를 보며 어떤 식으로 사용하는지 중점적으로 살펴봐야겠다.
기타치는 곰돌이, 주스탠드는 클라이언트의 전역 상태 관리를 도와주는 라이브러리다.
이번 과제에는 생각보다 전역 상태 관리를 사용할 일이 많지 않았다(내가 활용을 잘 못한 건가?).
그러다가 팀원 분께서 전역 상태 관리로 여러 모달을 한 번에 관리하는 어썸한 방법을 알려주셨다.
import { create } from "zustand/index"
interface UseModalStoreProps {
content: React.ReactNode
isOpen: boolean
openModal: (content: React.ReactNode) => void
closeModal: () => void
}
export const useModalStore = create<UseModalStoreProps>((set) => ({
content: null,
isOpen: false,
openModal: (content) => set(() => ({ isOpen: true, content })),
closeModal: () => set(() => ({ isOpen: false, content: null })),
}))
먼저 스토어에 넣을 타입을 정의하고 스토어를 생성한다.
여기서 content
가 모달에 들어갈 내용들이다.
import { useModalStore } from "@shared/model"
import { Dialog } from "@shared/ui"
export function Modal() {
const { isOpen, content, closeModal } = useModalStore()
return (
<Dialog open={isOpen} onOpenChange={closeModal}>
{content}
</Dialog>
)
}
그리고 Modal
컴포넌트를 만든다.
import { Button, CardHeader, CardTitle } from "@shared/ui"
import { PostAdd } from "@features/post/ui"
import { Plus } from "lucide-react"
import { useModalStore } from "@shared/model"
export function PostManagerHeader() {
const { openModal } = useModalStore()
const handleClickAddPost = () => {
openModal(<PostAdd />)
}
return (
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span>게시물 관리자</span>
<Button onClick={handleClickAddPost}>
<Plus className="w-4 h-4 mr-2" />
게시물 추가
</Button>
</CardTitle>
</CardHeader>
)
}
모달을 열어줄 함수를 위처럼 선언한다.
import { Card } from "@shared/ui"
import { Modal } from "@shared/ui/modal"
import { PostManagerHeader, PostManagerContent } from "@widgets/post/ui"
const PostsManager = () => {
return (
<>
<Card className="w-full max-w-6xl mx-auto">
<PostManagerHeader />
<PostManagerContent />
</Card>
<Modal />
</>
)
}
export default PostsManager
마지막으로 App
컴포넌트 또는 페이지 컴포넌트 등 모달을 띄울 상위 컴포넌트에 위와 같이 Modal
컴포넌트를 불러와 사용해주면 된다.
과제에 게시물 상세 보기, 추가, 수정, 댓글 추가, 수정 등 여러 화면이 Modal
형식으로 되어있어 요긴하게 써먹었다.
이번 과제의 주제는 FSD였다.
과제에 적용하면서 생긴 나름의 기준은
위와 같다.
이번 과제에서는 리액트 쿼리와 주스탠드 사용, 그리고 코드를 풀 때마다 나는 오류를 해결에 집중했다.
그러느라 FSD에 고민하면 시간이 지체될 거라 생각해 위와 같은 기준을 세우고 빠르게 분류했다.
클린 코드 과정은 시간 투자를 많이 해야하는 챕터였다.
정답이 정해져 있지 않고, '이게 맞을까?'라는 의구심.
'여기서 좀만 더 하면 괜찮아 질 것 같은데?' 무한 루프.
최근에 사이드 프로젝트를 시작하게 됐고, 함수형 프로그래밍 스터디도 잘 되고 있다.
코딩 테스트는 알고리즘을 공부해야 하는데... 왜이렇게 하기 싫은지... ㅎㅎ
아무튼 과제 + 사이드 프로젝트 + 스터디만 하는 데도 정신이 없다.
그러면서 현업과 병행하는 분들이 참 대단하다고 생각했다.
부지런하게 살기 위해 해야 할 일들을 벌이고 수습하는 방법이 나와 잘 맞다라는 생각을 했다.
설에도 같은 팀원 분들과 테스트 코드, 이력서 쓰기를 하기로 했다.
사놓고 듣지 않은 채로 모셔둔 강의(혼자 하면 안 한다는 반증)를 이번 기회에 듣고 잘 소화시켜 봐야겠다.
적당히 내가 감당할 정도로 벌리는 습관?을 들이는 것도 괜찮겠다?!
수요일에 돌이켜보니 과제를 하나도 안 했다고 생각했다.
하긴 했는데, 막상 진행된 게 없었다.
그래서 부랴부랴 하다가 밤을 샜다.
밤을 새며 '새벽에 집중이 잘 되는 구나, 생활 패턴을 바꾸는 것도 나쁘지 않겠다'고 생각했다.
다음날 늦잠을 잤고 잔 시간을 생각해보니 평소에 자는 시간과 똑같았다.
밤을 샜다는 보상 심리 때문인지, 하루를 나태하게 보냈다.
밤을 샌 다음날 항상 면역력이 떨어진 것 같은 느낌이 든다.
감기 기운은 아닌데, 걸릴 것 같은 몸상태?!
이런 걸 보면 사람은 낮에 활동을 해야 좋은가보다 싶다.
유튜브 쇼츠에 절여져 다섯 줄 이상의 글을 읽기 힘들다.
매주 긴 회고글을 쓰면서 남의 글을 못 읽는다니 사람 참 이기적이다.
공부를 하다보니 천천히 소화하면서 깊게 내려가고 싶은 생각이 든다.
그럴 수 있는 매개체는 글이지 않을까?
아님 말구 ㅋ
꼭 기술 서적이 아니더라도, 틈틈히 책을 좀 봐야겠다.