-
Notifications
You must be signed in to change notification settings - Fork 3
๐ฅ ํต์ฌ ๊ธฐ์
Jay edited this page Aug 8, 2023
·
7 revisions
ํ์๋ค ๋ชจ๋ React ํ๋ก์ ํธ๊ฐ ์ฒ์์ด์๊ธฐ ๋๋ฌธ์ React์์ ๋ง๋ค์ด์ ๋ฌธ๋ฒ ์นํ์ ์ธ ์ํ ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ Recoil
์ ์ ์ญ์์ ์ฌ์ฉํ๊ธฐ๋ก ๊ฒฐ์ .
- ํ์๋ค๊ณผ ํจ๊ป JD๋ฅผ ๋ถ์ํด๋ณธ ๊ฒฐ๊ณผ ํ์ฌ ํ๋ก ํธ์๋ ์
๊ณ์์ ์๊ตฌํ๋ ์ญ๋ ์ค
Fetch
ํจ์๋ฅผ ์คํํ ๋ ๋ฐ๋ณต๋๋ ์ฝ๋๋ฅผ ๊ฐ๊ฒฐํ๊ฒ ๋ง๋ค์ด์ฃผ๋axios
์React Query
๋ฅผ ์ฐ๋ํ๋ ๊ฒ์ ํ์ธ. - ์ํ ๊ด๋ฆฌ๋ฅผ ํ๋์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก๋ง ์งํํ๋ ๊ฒ์ด ์๋๋ผ ๊ฐ๊ฐ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์ฅ์ ์ ์ด๋ ค Client State๋ฅผ ๊ด๋ฆฌํ๋ Recoil๊ณผ ServerState๋ฅผ ๊ด๋ฆฌํ๋ ReactQuery๋ฅผ ์กฐํฉํ๋ ์ ๋ต์ ์ํ.
- Client๊ฐ ์์ ํ๋ฉฐ ์จ์ ํ ์ ์ด
- ํญ์ Client ๋ด๋ถ์ ์ ์ฅ๋๋ฉฐ ์กฐ์์ ๋ฐ๋ผ ์ต์ ์ํ๋ก ๊ฐฑ์
-
Recoil
์ React์์ ๋ง๋ ์ํ ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ -
key
๋ก ๊ตฌ๋ถ๋๋ ๊ฐ atom์, ์ฌ๋ฌ ์ปดํฌ๋ํธ์์ atom์ ๊ตฌ๋ ํ๊ณ ์๋ค๋ฉด ๊ทธ ์ปดํฌ๋ํธ๋ค๋ ๋์ผํ ์ํ๋ฅผ ๊ณต์ ํ๋ฏ๋ก atom์ด ์ ๋ฐ์ดํธ ๋๋ฉด, ํด๋น atom์ ๊ตฌ๋ ํ๊ณ ์๋ ๋ชจ๋ ์ปดํฌ๋ํธ๋ค์ยstate
๊ฐ ์๋ก์ด ๊ฐ์ผ๋ก ๋ฆฌ๋ ๋๋ง. - ํ์์ ์ ๋ณด๋ฅผ ์ ์ฅํ๋
userDataAtom
ํ์์ ๋ก๊ทธ์ธ ์ ๋ฌด์ API ํต์ ์ ์ํ Token์ ์ ์ฅํ๋privateDataAtom
์ ์ ์ญ์์ ํ์ฉํ ์ ์๋๋ก ์ ์ฅ. - ํ๋ก์ ํธ๋ฅผ ์งํํ๋ฉฐ
Modal
๋ด์ค๋ ํฐ๋ฅผ ์ํ EarthUs ๊ณต์ ๊ณ์ ์ ๋ณด
์ ๋ํ ์ํ๋ฅผ ์ถ๊ฐ๋ก ์ ์ญ์ ์ ์ฅ.
Fetching
Updating
๋น๋๊ธฐ API ์คํ- Client๊ฐ ์ ์ดํ๊ฑฐ๋ ์์ ํ์ง ์๋ ์๊ฒฉ์ Server ๊ณต๊ฐ์์ ๊ด๋ฆฌ
-
React Query
๋ ๋ฐ๋ณต์ ์ธ ๋น๋๊ธฐ API ํต์ ์ ์ฝ๊ฒ ์ฒ๋ฆฌํ๊ณ ์ ๋ฐ์ดํธ, ์บ์ฑ, ์๋ฌ์ฒ๋ฆฌ ๋ฑ์ ์ฝ๊ฒ ์ฒ๋ฆฌํ ์ ์๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ. - DevTool์ ์ ๊ณตํด์ฃผ๊ธฐ ๋๋ฌธ์ API ํต์ ์ ๋ํ ๋๋ฒ๊น ์ ์ฉ์ด.
- ๋น๋๊ธฐ๋ก ์๋ฒ ํต์ ์ ์ฑ๊ณตํ๋ฉด
data
๋ฅผ ๋ฐํํ๊ณ , ์๋ฌ๊ฐ ๋ฐ์ํ๋ฉดerror
๊ฐ์ฒด๋ก ๋ฐ์ดํฐ๋ฅผ ๋ฐํํ๋ ์ ํธ ๊ธฐ๋ฅ์ ํ๋ก์ ํธ์ ๋์ ํ๊ธฐ๋ก ๊ฒฐ์ .
- API ์์ฒญ๊ณผ ์๋ต ๋ฐ์ดํฐ ๋ก์ง์ ๊ฐ๊ฒฐํ๊ฒ ์์ฑํ ์ ์๋๋ก
axios
,ReactQuery
์ฌ์ฉ - ๊ฐ ํ์ด์ง๋ง๋ค ํต์ ์ ํ ๋ API ๋ช ์ธ๋ฅผ ๊ธฐ์ค์ผ๋ก ์๋์ ๊ฐ์ด ์ปค์คํ ํ ์ 4๊ฐ์ง๋ก ๋ถ๋ฅํด์ ์ ์.
-
useApiQuery
- ์ฌ์ฉ์์๊ฒ ๋ฐ์ดํฐ๋ฅผ ๋ณด์ฌ์ฃผ๋ HTTP METHODย
GET
ย ์์ฒญ๊ณผ ๊ฐ์ด ์๋ฒ์ ์ ์ฅ๋์ด ์๋ ์ํ๋ฅผ ํธ์ถํด์ ์ฌ์ฉํ ๋ ์ฌ์ฉ. - ์ปค์คํ ํ ์๋ ๋ฐฉ์
- ์ฌ์ฉ์์๊ฒ ๋ฐ์ดํฐ๋ฅผ ๋ณด์ฌ์ฃผ๋ HTTP METHODย
ํ๋ผ๋ฏธํฐ | ์ญํ |
---|---|
queryKey | ์ ์ก Url์ผ๋ก ์๋์ผ๋ก ์์ฑ๋๋๋ก ์ค๊ณ. |
queryFn | Query ์์ฒญ์ ์ํํ๊ธฐ ์ํ Promise๋ฅผ Return ํ๋ ํจ์. |
options | useQuery์์ ์ฌ์ฉ๋๋ refetchOnWindowFocus , enabled Option ๊ณตํต ์ ์ฉ. |
// useQuery ๊ตฌ์กฐ
const { isLoading, error, data } = useQuery(
[apiUrl], // queryKey
executeQuery, // queryFn
{ // Options
// ๋ธ๋ผ์ฐ์ ํ๋ฉด์ ์ดํํ๋ค๊ฐ ๋ค์ ํฌ์ปค์คํ ๋ refetch ๋ฐฉ์ง
refetchOnWindowFocus: false,
// ์ฟผ๋ฆฌ ์คํ ์ฌ๋ถ๋ฅผ Boolean์ ํตํด ์ ์ด
enabled,
},
);
-
- ์ฌ์ฉ์์๊ฒ ๋ฐ์ดํฐ๋ฅผ ๋ณด์ฌ์ฃผ๋ HTTP METHODย
GET
ย ์์ฒญ๊ณผ ๊ฐ์ด ์๋ฒ์ ์ ์ฅ๋์ด ์๋ ์ํ๋ฅผ ํธ์ถํด์ ์ฌ์ฉํ ๋ ์ฌ์ฉ. - ์ปค์คํ ํ ์๋ ๋ฐฉ
- ๋ฆฌ์กํธ ์ฟผ๋ฆฌ useQuery์
queryFn
์์ญ์excuteQuery
ํจ์๋ฅผ ์ฝ๋ฐฑ์ผ๋ก ์ ๋ฌ. -
์ ์ก Url
,๋ฉ์๋
,body(json)
,enabled
๋ฅผ ์ธ์๋ก ๋ฐ์ ํaxios
๋ฅผ ํ์ฉํด์ API์ ์์ฒญ ์ ์ก. - API ๋ช ์ธ์ ๊ณตํต์ผ๋ก ๋ค์ด๊ฐ๋ headers๋ ๊ณ ์ ์ผ๋ก ์ค์ .
- ์๋ต์ ์ฑ๊ณตํ ๊ฒฝ์ฐ
isLoading
error
data
๋ก ๋ฐํ.
- ์ฌ์ฉ์์๊ฒ ๋ฐ์ดํฐ๋ฅผ ๋ณด์ฌ์ฃผ๋ HTTP METHODย
// ์ ์ฒด ์ฝ๋ ๋ก์ง
const executeQuery = async () => {
try {
const headers = {
"Content-type": "application/json",
Authorization: `Bearer ${token}`,
};
const res = await axios({
url: BASE_URL + apiUrl,
method: "",
headers,
data: body,
enabled: true,
});
if (res.status === 200) {
return res.data;
}
} catch (error) {
return error;
}
};
const { isLoading, error, data } = useQuery(...);
return { isLoading, error, data };
-
useApiMutation
- ์ฌ์ฉ์๊ฐ ๋ฐ์ดํฐ๋ฅผ ์ ์ดํ๋ HTTP METHOD
POST
,PUT
,DELETE
์์ฒญ๊ณผ ๊ฐ์ด ์๋ฒ์ Side Effect๋ฅผ ๋ฐ์์์ผ ์ํ์ ๋ณ์ด๋ฅผ ๋ฐ์์ํฌ ๋ ์ฌ์ฉ. - ์ปค์คํ ํ ์๋ ๋ฐฉ์
- ์ฌ์ฉ์๊ฐ ๋ฐ์ดํฐ๋ฅผ ์ ์ดํ๋ HTTP METHOD
ํ๋ผ๋ฏธํฐ | ์ญํ |
---|---|
mutationKey | ์ ์ก Url์ผ๋ก ์๋์ผ๋ก ์์ฑ๋๋๋ก ์ค๊ณ. |
mutationFn | Mutation ์์ฒญ์ ์ํํ๊ธฐ ์ํ Promise๋ฅผ Return ํ๋ ํจ์. |
options | useMutation์์ ์ฌ์ฉ๋๋ Option ๊ฐ์ฒด๋ก ํ์ํ ๊ฒฝ์ฐ ํ์ด์ง์์ ์ง์ ์์ฑํด์ ์ฌ์ฉ. |
// useMutation ๊ตฌ์กฐ
const mutations = useMutation(
executeMutation, { // mutationFn
mutationKey: apiUrl, // mutationKey
...options, // options
});
-
- ๋ฆฌ์กํธ์ฟผ๋ฆฌ useMutation์
mutationFn
์์ญ์executeMutation
ํจ์๋ฅผ ์ฝ๋ฐฑ์ผ๋ก ์ ๋ฌ. -
์ ์ก Url
,๋ฉ์๋
,data
,options
๋ฅผ ์ธ์๋ก ๋ฐ์ ํaxios
๋ฅผ ํ์ฉํด์ API์ ์์ฒญ ์ ์ก. - API ๋ช ์ธ์ ๊ณตํต์ผ๋ก ๋ค์ด๊ฐ๋ headers๋ Token์ด ์๋์ง ์๋ณํ ํ ๊ณ ์ ์ผ๋ก ์ค์ .
- ์ธ๋ถ ํ์ผ์์ ์ปค์คํ
ํ
ํธ์ถ์ ์
๋ ฅํผ์ ์์ฑํ ํ ๋ฒํผ์ ์ด๋ฒคํธ ํธ๋ค๋ฌ์
mutate
ํจ์๋ฅผ ์ถ๊ฐํ๋ฉด ์ด๋ฒคํธ ๋ฐ์์ SideEffect ๋ฐ์.
- ๋ฆฌ์กํธ์ฟผ๋ฆฌ useMutation์
// ์ ์ฒด ์ฝ๋ ๋ก์ง
export default function useApiMutation(
apiUrl = "", method = "", data = null, options = {},
) {
const executeMutation = async () => {
const headers = {
"Content-type": "application/json",
};
token && (headers.Authorization = `Bearer ${token}`);
const res = await axios({
url: BASE_URL + apiUrl,
method,
headers,
data,
});
return res.data;
};
const mutations = useMutation(executeMutation, {...});
return mutations;
}
// ๊ฒ์๋ฌผ ์
๋ก๋ ํธ๋ค๋ฌ ์ด๋ฒคํธ ์์
const handleUploadPost = e => {
e.preventDefault();
uploadPostMutation.mutate();
};
-
useImageUploader
-
useApiMutatuion
Hook ๊ณผ ๊ฐ์ด ์๋ฒ์ Side Effect๋ฅผ ๋ฐ์์์ผ ์ํ์ ๋ณ์ด๋ฅผ ๋ฐ์์ํค๋ฉฐ<input type=โfileโ/>
์ ํตํดimage
ํ์ฅ์๋ฅผ ์ ๋ก๋ํ๋ ๊ฒฝ์ฐ ์ฌ์ฉ. - API ๋ช
์ธ์์ ์ด๋ฏธ์ง ๋ฑ๋ก์ด ํ์ํ ํ์ด์ง์์๋ ์๋ฒ์ ์ด๋ฏธ์ง๋ฅผ ์ ์กํ๋ฉด ์ซ์๋ก ์ด๋ฃจ์ด์ง
filename
์ ํฌํจํ๋ ์๋ต์ ๋ฐํํ๋ฏ๋ก ํด๋น ๊ธฐ์ค์ ๋ง์ถฐ ์กฐ๊ฑด์ ์คํ. - ์ปค์คํ ํ ์๋ ๋ฐฉ์
-
ํ๋ผ๋ฏธํฐ | ์ญํ |
---|---|
mutationKey | ์ ์ก Url์ผ๋ก ์๋์ผ๋ก ์์ฑ๋๋๋ก ์ค๊ณ. |
mutationFn | Mutation ์์ฒญ์ ์ํํ๊ธฐ ์ํ Promise๋ฅผ Return ํ๋ ํจ์. |
options | useMutation์์ ์ฌ์ฉ๋๋ Option ๊ฐ์ฒด๋ก ํ์ํ ๊ฒฝ์ฐ ํ์ด์ง์์ ์ง์ ์์ฑํด์ ์ฌ์ฉ. |
// ๋ก์ปฌ ์ด๋ฏธ์ง ์๋ฒ์ ๋ฑ๋ก
const executeImageUpload = async files => {
const formData = new FormData();
// files๊ฐ ๋ฐฐ์ด์ธ์ง ์๋์ง ํ์ธ ํ ๋ฐฐ์ด์ด ์๋๋ฉด ๋ฐฐ์ด๋ก ๋ณํ
const filesArray = Array.isArray(files) ? files : [files];
// ๋ฐฐ์ด์ ์๋ ๋ชจ๋ ํ์ผ์ formData์ 'image'๋ผ๋ ํค๋ก ์ถ๊ฐ
filesArray.forEach(file => {
formData.append("image", file);
});
// axios๋ก formData๋ฅผ ์ ์กํด์ data ๋ฐํ
const response = await axios.post(BASE_URL + apiUrl, formData);
return response.data;
};
// useMutation ๊ตฌ์กฐ
const mutation = useMutation(executeImageUpload, {
onSuccess: data => {
// ๋ฐํ ๋ฐ์ data๊ฐ ๋ฐฐ์ด์ธ์ง ์๋ณ ํ filename ์ถ์ถ
let filenames;
(Array.isArray(data)) {
filenames = data.map(d => d.filename);
} else {
filenames = [data.filename];
}
// ์ด๋ฏธ์ง ์ฃผ์๋ฅผ BASE_URL๊ณผ ์กฐํฉํด์ image ์ํ์ ์ ์ฅ.
const apiImg = filenames
.map(filename => `${BASE_URL}/${filename}`)
.join(",");
setImage(apiImg);
},
// ์ ์ฒด ์ฝ๋ ๋ก์ง
export default function useImageUploader(apiUrl) {
const [image, setImage] = useState("");
const executeImageUpload = async files => {...};
const mutation = useMutation(...);
return { mutation, image };
}
-
- ๋ฆฌ์กํธ์ฟผ๋ฆฌ useMutation์
mutationFn
์์ญ์executeImageUpload
ํจ์๋ฅผ ์ฝ๋ฐฑ์ผ๋ก ์ ๋ฌ. -
์ ์ก Url
,formData
๋ฅผ ์ธ์๋ก ๋ฐ์ ํaxios
๋ฅผ ํ์ฉํด์ API์ ์์ฒญ ์ ์ก. - ํ์ผ์ 2์ฅ ์ด์ ๋ฑ๋กํ์ ๊ฒฝ์ฐ
filename
์ด ๋ฐฐ์ด๋ก ๋ฐํ๋๋ฏ๋ก, ๋จ์ผ์ด๋ฏธ์ง์ ๋์ผํ ๋ก์ง์ผ๋ก ์ฒ๋ฆฌํ๊ธฐ ์ํด.join
๋ฐฐ์ด๋ฉ์๋๋ฅผ ํตํด ๋ฌธ์์ด์ ํ๋๋ก ์ทจํฉํ์ฌimage
๋ณ์์ ์ ์ฅ.
- ๋ฆฌ์กํธ์ฟผ๋ฆฌ useMutation์
-
- ์ฌ์ฉ์์๊ฒ ๋ฐ์ดํฐ๋ฅผ ๋ณด์ฌ์ฃผ๋ HTTP METHODย
GET
ย ์์ฒญ๊ณผ ๊ฐ์ด ์๋ฒ์ ์ ์ฅ๋์ด ์๋ ์ํ๋ฅผ ๊ณ์ํด์ ํธ์ถํด์ ์ฌ์ฉํ ๋ ์ฌ์ฉ. - ์ฌ์ฉ์๊ฐ ์คํฌ๋กคํ ๋๋ง๋ค ์๋ก์ด ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ๋ฌดํ ์คํฌ๋กค ๊ตฌํ์ ์ฌ์ฉ.
-
react-infinite-scroller
๋ผ์ด๋ธ๋ฌ๋ฆฌ์InfiniteScroll
์ปดํฌ๋ํธ์ ๋์ ํธํ์ฑ. - ์ปค์คํ ํ ์๋ ๋ฐฉ์
- ์ฌ์ฉ์์๊ฒ ๋ฐ์ดํฐ๋ฅผ ๋ณด์ฌ์ฃผ๋ HTTP METHODย
ํ๋ผ๋ฏธํฐ | ์ญํ |
---|---|
queryKey | ์ ์ก Url์ผ๋ก ์๋์ผ๋ก ์์ฑ๋๋๋ก ์ค๊ณ. |
queryFn | InfiniteQuery ์์ฒญ์ ์ํํ๊ธฐ ์ํ Promise๋ฅผ Return ํ๋ ํจ์. pageParam ํ๋ผ๋ฏธํฐ ์ฌ์ฉ. |
options | useInfiniteQuery์์ ์ฌ์ฉ๋๋ getNextPageParam Option ๊ณตํต ์ ์ฉ. |
// useInfiniteQuery ๊ตฌ์กฐ
const { data, hasNextPage, fetchNextPage, isLoading, isError } =
useInfiniteQuery(
[apiUrl], // queryKey
executeInfiniteQuery, // queryFn
{ // Options
// ๋ค์ API ์์ฒญ์ ์ฌ์ฉ๋ pageParam ๋ฐํ
// ํ ํ์ด์ง์ ๋ฐ์ดํฐ ๊ธธ์ด๊ฐ LIMIT๋ณด๋ค ์์ ๊ฒฝ์ฐ undefined ๋ฐํ
// ๊ทธ ์ธ์ ๊ฒฝ์ฐ ํ์ฌ ํ์ด์ง ์ * LIMIT ๋ฐํ
getNextPageParam: (lastPage, allPages) => {
const resKey = keyName;
if (
keyName
? lastPage[resKey] && lastPage[resKey].length < LIMIT
: lastPage && lastPage.length < LIMIT
) {
return undefined;
}
return { skip: allPages.length * LIMIT };
},
}
);
-
- ๋ฆฌ์กํธ ์ฟผ๋ฆฌ useInfiniteQuery์
queryFn
์์ญ์excuteInfiniteQuery
ํจ์๋ฅผ ์ฝ๋ฐฑ์ผ๋ก ์ ๋ฌ. -
excuteInfiniteQuery
ํจ์์์pageParam
์ ์ธ์๋ก ์ ๋ฌํ์ฌ API ์์ฒญ ์ ์ก Url์ skip ํ๋ผ๋ฏธํฐ ๊ฐ์ผ๋ก ์ฌ์ฉ. -
์ ์ก Url
,์๋ต key ๊ฐ
์ ์ธ์๋ก ๋ฐ์ ํaxios
get
๋ฉ์๋๋ฅผ ํ์ฉํด์ API์ ์์ฒญ ์ ์ก. - API ๋ช ์ธ์ ๊ณตํต์ผ๋ก ๋ค์ด๊ฐ๋ headers๋ ๊ณ ์ ์ผ๋ก ์ค์ .
- ์๋ต์ ์ฑ๊ณตํ ๊ฒฝ์ฐ
data
hasNextPage
fetchNextPage
isLoading
isError
๋ก ๋ฐํ.
- ๋ฆฌ์กํธ ์ฟผ๋ฆฌ useInfiniteQuery์
// ์ ์ฒด ์ฝ๋ ๋ก์ง
const executeInfiniteQuery = async ({ pageParam = { skip: 0 } }) => {
const { skip } = pageParam;
try {
const headers = {
"Content-type": "application/json",
Authorization: `Bearer ${token}`,
};
const res = await axios.get(
`${BASE_URL + apiUrl}?limit=${LIMIT}&skip=${skip}`,
{ headers },
);
if (res.status === 200) {
console.log("์์ฒญ์ ์ฑ๊ณตํ์ต๋๋ค.");
console.table(res.data);
return res.data;
}
} catch (error) {
return error;
}
};
const { data, hasNextPage, fetchNextPage, isLoading, isError } =
useInfiniteQuery(...);
return { data, hasNextPage, fetchNextPage, isLoading, isError };
-
-
hasNextPage
fetchNextPage
๋InfiniteScroll
์ปดํฌ๋ํธ์์ ์ฌ์ฉ.hasMore
๊ฐ true์ธ ๊ฒฝ์ฐloadMore
๋ฅผ ์คํํ์ฌ ๋ฐ์ดํฐ ๋ก๋.
-
<InfiniteScroll hasMore={hasNextPage} loadMore={() => fetchNextPage()} />
- ๊ฒ์ API๋ฅผ ํตํ ๊ธฐ๋ฅ ๊ตฌํ์์ ์ผ์นํ๋ ๊ฒ์์ด์ ํ์ด๋ผ์ดํธ๋ฅผ ํ์ํ ๋ ์ฌ์ฉ.
-
<mark>
๋ ์ฌ์ฉ์์ ํ์ฌ ํ๋๊ณผ ์ฐ๊ด์ด ์๋ ๋ถ๋ถ์ ๋ํ๋ด๋ฉฐ ์ฃผ์๋ฅผ ๋๊ธฐ ์ํด ์ฌ์ฉํ๋ ํ๊ทธ. - ๋ฐ๋ผ์, ๊ฒ์ ๊ฒฐ๊ณผ ํ
์คํธ์ ์๋งจํฑํ ์๋ฏธ๋ฅผ ๋ถ์ฌํ๊ธฐ ์ํด
<mark>
๋ฅผ ์ ์ฉํด ์คํ์ผ๋ง.
// Link ๋๋๋ง ์กฐ๊ฑด๋ถ ์ถ๋ ฅ
function renderLinkContent() {
// Search ํ์ด์ง ํ์ด๋ผ์ดํธ ๊ด๋ จ
if (SEARCH) {
const regex = new RegExp(searchKeyword, "gi");
const userNameWithHighlight = userName.replace(
regex,
match => `<mark class="highlight">${match}</mark>`,
);
return (
<>
<Avatar profileImg={profileImg} size={40} />
<div>
<span dangerouslySetInnerHTML={{ __html: userNameWithHighlight }} />
{id ? <p>@{account}</p> : ""}
</div>
</>
);
}
return null;
}
-
๊ธ๊ผด ๋ฐ ๋ฐฐ๊ฒฝ ์ ๋ช ๋ ๋๋น ์ค์.
-
๋ฒํผ์ ์์ด์ฝ์ผ๋ก๋ง ๋ณด์ฌ์ฃผ๋ ๊ฒ ์๋๋ผ ๋ฌธ์๋ฅผ ์ฌ์ฉํ์ฌ ์ง๊ด์ ์ผ๋ก ์๋ฏธ ์ ๋ฌ.
- swiper slide ํญ์ผ๋ก ํ์ด์ง ์ ํ ๊ฐ๋ฅ.
- ๋ชจ๋ฌ์ด ์ด๋ ธ์ ๋ ๋ชจ๋ฌ ์์์ ํฌ์ปค์ค ์์ง ์๊ณ ์ด๋.
-
<input type=โfileโ />
์ ํ์ผ ์ ํ ์ปค์คํฐ๋ง์ด์ง ๋ฒํผ ํญ ํค๋ก ์ ๊ทผ ๊ฐ๋ฅ.
<input type=โfileโ />
์ ํ์ผ ์ ํ ๋ฒํผ์ ์ปค์คํฐ๋ง์ด์งํ๋๋ฐlabel
ํ๊ทธ ์์button
ํ๊ทธ๋ฅผ ์ฌ์ฉํ ์ ์์ดdiv
ํ๊ทธ์ ํญ ํค๋ก ์ ๊ทผ ๊ฐ๋ฅํ๋๋กrole=โbuttonโ
tabIndex={0}
์์ฑ ์ถ๊ฐ.
- ๊ฒ์ ๊ธฐ๋ฅ ์ฌ์ฉ ์, ๊ฒ์์ด์ ์ผ์นํ๋ ํ์ด๋ผ์ดํธ ๋ถ๋ถ์ ์๊ฐ ๋ฟ๋ง ์๋๋ผ ์ฒญ๊ฐ์ผ๋ก๋ ์ธ์งํ ์ ์๊ฒ ์์ฑ ๊ฐ์กฐ ํจ๊ณผ ์ถ๊ฐ.
mark::before,
mark::after {
clip-path: inset(100%);
clip: rect(1px, 1px, 1px, 1px);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}
mark::before {
content: " [๊ฐ์กฐ ์์] ";
}
mark::after {
content: " [๊ฐ์กฐ ๋] ";
}
- ์ฑํ ๋ฐฉ์ ์๋ก์ด ๋ฉ์ธ์ง ์ ์ก ์, ์ค์๊ฐ์ผ๋ก ์ ๋ฐ์ดํธ ๋๋ ๋ฉ์ธ์ง ๋ ธ๋๋ฅผ ์คํฌ๋ฆฐ ๋ฆฌ๋๊ฐ ์ฝ์ ์ ์๊ฒ aria-live ๋ฐ role ์์ฑ ์ ์ฉ.
<section
className="sendBubble"
role="log"
aria-live="polite"
aria-label="Chat"
>
{messages.map(message => (
<ChatBubble
key={message.key}
isReceived={false}
sentMessage={message.content}
uploadedImage={message.uploadedImage}
currentTime={message.time}
/>
))}
</section>
- ํผ๊ทธ๋ง
Variable
๊ธฐ๋ฅ์ ํ์ฉํ์ฌ ๋ฒํผhover
focus
๋ฑ ๋ค์ํ ํ์ ์ ๋์ํ ์ ์๋๋ก ๋์์ธ ์์คํ ์ ์. -
StyleComponent
๋ฅผ ํ์ฉํด์ ๋ฒํผ ์ปดํฌ๋ํธ๋ฅผ ์ ์ํ๊ณ props๋ฅผ ํตํด ํ์ ์ ์ ์ํ์ฌ ์ฌ์ฌ์ฉ์ฑ ์ฆ์ง.
<Button
size="cta"
variant={!disabledBtn && "primary"}
type="submit"
>
๋ก๊ทธ์ธ
</Button>
const VARIANTS = {
primary: css`
--button-color: var(--color-white);
--button-background: var(--color-primary);
--button-border: 1px solid var(--color-primary);
&::before {
filter: invert(1);
}
`,
white: css`
--button-color: var(--color-black);
--button-background: var(--color-white);
--button-border: 1px solid var(--color-light);
&::before {
filter: invert(36%) sepia(93%) saturate(1033%) hue-rotate(183deg)
brightness(88%) contrast(89%);
}
`,
};
const SIZES = {
sm: css`
--button-font-size: var(--font-size-xs);
--button-padding: 0 1rem;
--button-height: 2.25rem;
`,
md: css`
--button-font-size: var(--font-size-md);
--button-padding: 0 4rem;
--button-height: 3rem;
`,
lg: css`
--button-font-size: var(--font-size-lg);
--button-padding: 0 6rem;
--button-height: 4rem;
`,
cta: css`
--button-font-size: var(--font-size-xl);
--button-padding: 0 1rem;
--button-height: 4rem;
position: fixed;
left: 50%;
transform: translateX(-50%);
bottom: 1rem;
width: min(calc(100% - 2rem), calc(var(--size-max-width) - 2rem));
border-radius: 0;
align-items: center;
justify-content: center;
z-index: 10;
`,
};
export default function Button({
size,
variant,
children,
icon,
disabled,
...props
}) {
return (
// eslint-disable-next-line react/jsx-props-no-spreading
<StyledButton
icon={icon}
size={size}
variant={variant}
disabled={disabled}
{...props}
>
<span>{children}</span>
</StyledButton>
);
}
const StyledButton = styled.button`
${({ variant }) => VARIANTS[variant]}
${({ size }) => SIZES[size]}
font:inherit;
cursor: pointer;
margin: 0;
background: var(--button-background);
border: var(--button-border);
color: var(--button-color);
padding: var(--button-padding);
height: var(--button-height);
line-height: var(--button-height);
font-size: var(--button-font-size);
border-radius: 0.25rem;
display: inline-flex;
justify-content: center;
align-items: center;
gap: 0.5rem;
transition: all 0.4s ease-out;
&:disabled {
--button-color: var(--color-gray-76);
--button-background: var(--color-bg);
--button-border: 1px solid var(--color-bg);
cursor: not-allowed;
pointer-events: none;
}
&:hover {
filter: brightness(0.9);
}
// icon์ด ์๋ ๊ฒฝ์ฐ
${({ icon }) =>
icon &&
css`
&::before {
content: "";
display: inline-block;
width: 1.5rem;
height: 1.5rem;
background: url(${props => props.icon}) no-repeat center/1.5rem;
`}
`;