타입을 검사한다는 기능은 개발 시에 필요하며, 배포시에는 굳이 필요가 없다.
따라서 개발환경에서만 실행되는 devDependencies에 깔면 성능최적화를 기대할 수 있다.
create-react-app의 타입스크립트 기본 템플릿을 사용할 경우 그냥 디펜던시에 타입스크립트 관련 모듈들이 깔아지는데 이것도 같이 옮겨놓으면 좋다.
또한 @types가 붙은 모듈들은 라이브러리가 아닌, 타입스크립트용으로 해당 라이브러리의 타입지정파일만을 모듈화 한것으로 역시 devDependencies에 깔아서 개발환경에서만 쓰면 좋다.
"@types/jest": "^27.0.1",
"@types/node": "^16.7.13",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"types": "^0.1.1",
"typescript": "^4.4.2"
export const getToDos = () =>
instance.get<ToDoList>(`/todos`).then((response) => response.data);
export const getToDoById = (id: String) =>
instance.get<ToDoList>(`/todos/${id}`).then((response) => response.data);
export const updateToDo = async (data: NewToDo, id: string) => {
const { data: response } = await instance.put(`/todos/${id}`, data);
return response.data;
};
export const deleteToDo = async (id: string) => {
const response = await instance.delete(`/todos/${id}`);
return response.data;
};
export const loginTodo = async (data: UserProps) => {
const { data: response } = await instance.post(`/users/login`, data);
return response.data;
const deleteMutation = useMutation(
(id: string) =>
instance.delete(`/todos/${id}`, {
headers: { Authorization: token },
}),
{
onSuccess: () => {
queryClient.invalidateQueries(["todos"]);
},
}
);
export const ToDosAPI = {
getToDos: () => instance.get<ToDoList>(`/todos`),
getToDoById: (id: String) => instance.get<ToDoDetail>(`/todos/${id}`),
createToDo: (create: NewToDo) => instance.post(`todos`, create),
updateToDo: async (update: NewToDo, id: string) =>
await instance.put(`/todos/${id}`, update),
deleteToDo: async (id: string) => await instance.delete(`/todos/${id}`),
};
export function getToDos() {
return useQuery(["todos"], () =>
ToDosAPI.getToDos().then((response) => response.data)
);
}
export function getToDoById() {
const queryClient = useQueryClient();
return useMutation((id: string) => ToDosAPI.getToDoById(id), {
onSuccess: () => queryClient.invalidateQueries(["todo"]),
});
}
export function createTodo() {
const queryClient = useQueryClient();
return useMutation((create: NewToDo) => ToDosAPI.createToDo(create), {
onSuccess: () => queryClient.invalidateQueries(["todos"]),
});
}
export function updateToDo(id: string) {
const queryClient = useQueryClient();
return useMutation((update: NewToDo) => ToDosAPI.updateToDo(update, id), {
onSuccess: () => queryClient.invalidateQueries(["todos"]),
});
}
export function deleteToDo(id: string) {
const queryClient = useQueryClient();
return useMutation(() => ToDosAPI.deleteToDo(id), {
onSuccess: () => queryClient.invalidateQueries(["todos"]),
});
}
instance.defaults.headers.common.Authorization = token || "";
이렇게 디폴트로 값을 넣어주었는데 첫 로그인이나 첫 회원가입에서 종종 토큰이 제대로 세팅되지 않으며 홈페이지로 넘어가면 오류를 일으켰다.
자바스크립트에서는 토큰이 없을 경우 null값으로 둘 수 있었는데 타입스크립트에서는 null을 허용하지 않아서 "" 빈값을 할당해 주어야 했고, 이 값이 갱신되지 않으면서 생기는 문제였다.
instance.interceptors.request.use(function (config) {
const token = localStorage.getItem("token");
if (token !== "")
instance.defaults.headers.common.Authorization = token || "";
return config;
});
<button className="bg-red-300 bg-green-400"></button>
이런게 안된다.
이는 앨리먼트에 무슨 옵션이 적용되어 있는지 확인하고 작업하거나, 혹은 앨리먼트 자체를 확장가능하게 최소한의 요소만으로 제작해야 한다는 뜻이었다.
편하려고 커스텀을 만들었는데 오히려 더 불편하고 button에 원래 있는 기능들 또한 props로 전달하기 위해 타입지정을 해주어야 했다.
import React from "react";
type ButtonType = {
children: React.ReactNode | React.ReactNode[];
className?: string;
onClick?: () => void;
disabled?: boolean;
};
const Button = (props: ButtonType) => {
const init = `bg-blue-200 w-[13.75rem] h-[3.125rem]
rounded-md shadow-md font-[1.25rem] text-white font-karla font-semibold
disabled:bg-gray-200`;
const { children, className, onClick, disabled } = props;
const [classNameList, setClassNameList] = React.useState(init);
React.useEffect(() => {
if (className) {
setClassNameList(classNameList.concat(` ${className}`));
}
}, []);
return (
<button className={classNameList} onClick={onClick} disabled={disabled}>
{children}
</button>
);
};
export default Button;
그래서 이번에는 새로운 패키지를 도입해보았다.
저번 프로젝트에서 PPT에 넣을 레퍼런스를 서치하다가 내가 고민하던 문제에 대한 팁이 올려진 기술블로그를 찾았었다.
블로그 : https://fe-developers.kakaoent.com/2022/220303-tailwind-tips/
twin.macro라는 라이브러리이다.
className 정렬 문제도 해결해주고, styled import 내에서 일반 css나 scss를 같이 사용할 수 있다는 것도 장점이다.
최근 테일윈드 3.0이 업데이트 되어서 아직은 베타버전만 릴리즈되어 있지만 일단은 잘 돌아가고 있다.
해당 이슈 : https://github.com/ben-rogerson/twin.macro/issues/589
기존 사용하던 tailwind-styled-component의 상위호환 라이브러리였기에 관련 디펜던시를 제거하고 twin.macro로 스펙을 통일시켰다.
또한 앨리먼트 자체가 아니라 css만 분리하는 새로운 방식을 도입하였는데, 공식 홈페이지에 예시가 무척 잘 나와있었기에 가져왔다.
import React from "react";
/** @jsxImportSource @emotion/react */
import tw, { css, styled, theme } from "twin.macro";
interface ButtonProps {
variant?: "primary" | "secondary";
isSmall?: boolean;
}
export const Button = styled.button(({ variant, isSmall }: ButtonProps) => [
tw`px-8 py-2 rounded focus:outline-none transform duration-75`,
tw`hocus:(scale-105 text-yellow-400)`,
variant === "primary" && tw`bg-black text-white border-black`,
variant === "secondary" && [
css`
box-shadow: 0 0.1em 0 0 rgba(0, 0, 0, 0.25);
`,
tw`border-2 border-yellow-600`,
],
isSmall ? tw`text-sm` : tw`text-lg`,
// The theme import can supply values from your tailwind.config.js
css`
color: ${theme`colors.white`};
`,
]);
const YellowButton = tw(Button)`border-yellow-500 border-4 text-amber-600`;
{token && <Navigate to="/todo" />}
해결 1 : customRoute를 만들어 Routes를 대체했다.
token 유무에 따라 리다이렉트 될 수 있게 useEffect를 걸어주었고, 매번 변화가 있을때 검사를 해야 했기 때문에 함수를 useCallback으로 만들어 거기에 디펜던시를 걸어주고, useEffect에서는 제거해주었다.
const CustomRoutes = () => {
const token = !!localStorage.getItem("token")?.valueOf();
const { pathname } = useLocation();
const navigate = useNavigate();
const auth = React.useCallback(
(token: boolean) => {
if (token === true && ["/", "/signin"].includes(pathname)) {
return navigate("/todo");
}
if (token === false && ["/todo"].includes(pathname)) {
return navigate("/");
}
},
[navigate, pathname]
);
React.useEffect(() => {
auth(token);
});
return (
<Routes>
{["/todo", "/todo/:id"].map((path) => {
return <Route path={path} element={<Home />} key={path} />;
})}
{["/", "/signin"].map((path) => {
return <Route path={path} element={<SignUp />} key={path} />;
})}
</Routes>
);
};
export default CustomRoutes;
export const ToDoInit = {
data: {
data: [
{
title: "",
content: "",
id: "",
createdAt: new Date(),
updatedAt: new Date(),
},
],
},
};
const { data } = token ? getToDos() : ToDoInit;
기존 쓰던 api 형태가 아니라 hook이었기 때문에 따로 useEffect를 지정해주지 않아 좋다고 생각했지만, 그것때문에 페이지 소환시 다른 어떤 것보다 먼저 실행 => 실패처리 되어 다른 컴포넌트의 로딩을 막고 있었다.
그리고 같은 데이터라는것도 보장받지 못해서 any를 써야했었다.
이렇게 타입을 한정해주자 깔-끔!
그치만 리액트쿼리 이론 강의를 들으면 좀더 좋은 처리방법이 생각날 것도 같다.
이런 느낌으로 form.tsx 컴포넌트 안에 로직들이 이리저리 뒤섞여있었다.
훅을 분리하긴 했지만 submit 함수는 inline으로 작성되었었다.
recoil로 토큰을 관리하려고 했던 흔적도 남아있었다. 로컬스토리지 자체가 이미 전역이므로 굳이 필요없는 로직이었다.
const location = useLocation();
const navigate = useNavigate();
const loginQuery = useLogin();
const [tokens, setTokens] = useRecoilState(tokenState);
const { values, errors, handleChange, handleSubmit, isError } = useForm(
location.pathname === "/sign"
? login
: location.pathname === "/signin"
? SignIn
: () => alert("알수 없는 오류가 발생했습니다. 다시 시도해주세요"),
validate
);
const token: string = localStorage.getItem("token") || "";
function login() {
loginQuery
.mutateAsync(values)
.then((res) => {
localStorage.setItem("token", res.data.token);
setTokens(res.data.token);
navigate("/");
})
.catch((error) => {
console.log(error);
alert("아이디와 비밀번호를 확인해주세요");
});
return <Navigate to="/" />;
}
const navigate = useNavigate();
const { pathname } = useLocation();
const [loading, setLoading] = useRecoilState(loadingState);
const isLoginPage = pathname === "/";
const isSignInPage = pathname === "/signin";
React.useEffect(() => {
setTimeout(() => {
setLoading(false);
}, 1000);
}, [loading]);
const { values, errors, handleChange, handleSubmit, isError } = useForm(
login,
validate
);
function login() {
if (isLoginPage) {
UserAPI.loginTodo(values)
.then((res) => {
localStorage.setItem("token", res.data.token);
})
.catch((error) => {
console.log(error);
alert("아이디와 비밀번호를 확인해주세요");
});
}
if (isSignInPage) {
UserAPI.singUpTodo(values).then((res) => {
localStorage.setItem("token", res.data.token);
alert("계정 생성 완료, 자동 로그인 되었습니다!");
});
}
setLoading(true);
setTimeout(() => {
navigate("/todo");
}, 300);
}
이 페이지만 보아도 전체 내용이 한눈에 들어올 수 있게 정리하기!
token을 한번에 받아오지 못하고 로그인페이지로 다시갔다가 홈으로 가는 이슈가 있었는데 저렇게 setTimeout을 걸어 약간 뒤로 밀어주니 깔끔하게 작동했다.
누적 레이아웃 이동(CLS)은 사용자가 예상치 못한 레이아웃 이동을 경험하는 빈도를 수량화한 것이다. 시각적 안정성을 측정할 때 중요한 ux 요소 중 하나이다.
label element를 커스텀하면서 label 자체에 기본 높이를 주어 에러메세지가 사라지거나 나타날때에도 CLS가 발생하지 않도록 개선하였다.
다른 페이지들을 모달로 띄우려고 했지만 과제 특성(각 상세페이지 뒤로가기 + 한 페이지 내에서 새로고침 없이 데이터 정합성 유지하기)을 고려했을 때 단일 페이지를 유지하는 것이 가장 깔끔해보였다.
이에 크리에이트 폼과 디테일페이지에 sticky 포지션을 적용해서 유저가 스크롤을 내릴 경우 따라오도록 만들었다.
인풋 앨리먼트에서 각각에 대한 css와 타입을 지정해준다음 불러와 사용했다. onChange에서 타입설정하는 부분이 어려웠지만 해냈다!
이렇게 두개를 지정해두면 한개의 함수로 돌려쓸 수 있다.
const handleChange = (
event:
| React.ChangeEvent<HTMLInputElement>
| React.ChangeEvent<HTMLTextAreaElement>
) => {
event.persist();
setValues((values) => ({
...values,
[event.target.name]: event.target.value,
}));
};
const modifyRef = React.useRef<HTMLInputElement>(null);
React.useEffect(() => {
if (modify) {
if (modifyRef.current && focus) {
modifyRef.current.focus();
setFocus(false);
}
}
});
p태그 / input태그의 모양과 위치를 비슷하게 맞추어 CLS를 최소화했다.
수정모드일 경우 글씨가 살짝 회색으로 변하고 상세설명 칸(텍스트에리어)에 최대한 많은 줄 수를 부여하기 위해 두 칸의 갭이 줄어든다.