플래시카드 웹 서비스입니다. 플래시카드는 효율적으로 암기하는 학습 도구입니다. 앞에 문제 뒤에는 정답을 작성해서 카드를 만듭니다. 문제를 틀리면 다시 풀고 맞추면 시간을 두고 다시 풀어봅니다. 맞출 때마다 다시 풀어보는 간격을 늘려갑니다.
현재는 1.0.0-alpha는 영단어 암기 위주로 기능을 제공하고 있습니다. 기능은 추가되고 확장될 것입니다. 아래 링크로 즐거운 학습 경험해보기 바랍니다.
회원가입이 꺼림직하다면 아래 공용 어드민 계정을 활용해볼 수 있습니다. 다른 채용담당자도 볼 수 있기 때문에 조심히 다루기 바랍니다.
qwer1234
- Nest.js로 porting전까지 활용할 백엔드 레포입니다.
- Deno deploy 무료 플랜을 활용하고 있습니다.
이 프로젝트는 폴리레포 프로젝트이고 fullstack 프로젝트입니다. MVP를 빠르게 만들고 유지보수하며 확장하기 위한 프로젝트입니다.
레퍼런스: React Query meets React Router - tkdodo
기다림 끝에 또 유저에게 기다림을 요구하는 것은 관공서로 충분합니다.
유저가 A를 요청하면 처리 후 A를 줘야 합니다. 하지만 request waterfall 현상은 유저가 A를 달라고 하면 처리하고 B를 잠시 두고 있다가 A를 주는 것과 같습니다. 굳이 2번 기다리게 하지말고 1번만 기다리게 해도 됩니다. 유저는 로그인을 위한 요청처리 이후 다시 본인 리소스에 대한 요청인 2번의 request-response 라이프 사이클을 알 필요 없습니다.
React-Router-DOM에서 loader는 Page 접근 전에 실행하는 함수입니다. 실행하고 싶은 로직을 콜백함수로 대입하고 콜백함수의 반환값도 useLoaderData
로 접근할 수 있습니다.
function Cards() {
const { cards, isLoading, error } = useCards();
return <>{/* ... 생략 */}</>;
}
로직이 중복해서 useCard custom hook으로 담습니다.
import { useQuery } from '@tanstack/react-query';
import { cardLoader, cardsQuery } from '@/utils';
import { useLoaderData } from 'react-router-dom';
export function useCards() {
const loaderCards = useLoaderData() as Awaited<
ReturnType<ReturnType<typeof cardLoader>>
>;
const query = cardsQuery();
const {
data: cards,
isLoading,
error,
} = useQuery({ ...query, initialData: loaderCards });
return { cards, isLoading, error };
}
useCards 내부에서는 useLoaderData의 결과 값을 react-query에 캐싱합니다.
import { cardsQuery } from '@/utils';
import queryClient from '@/libs/queryClient';
export const cardLoader = () => async () => {
const query = () => ({
queryKey: ['cards'],
queryFn: getCardsAPI,
staleTime: 5000,
});
return (
queryClient.getQueryData<Card[]>(query.queryKey) ??
(await queryClient.fetchQuery(query))
);
};
mount 하기 전에 query-cache는 캐싱하면 Page 컴포넌트 Mount에 요청을 해결할 수 있습니다.
- 로그인 시점에 1번만 기다리고 페이지를 방문할 수 있게 됩니다.
- 다른 페이지를 접근해도 불필요한 로딩 스피너가 보이지 않습니다.
- Vite으로 code splitting이 아주 간편하게 처리할 수 있습니다.
- 성능문제가 없는 Vitest를 1.1.0에 테스트러너로 활용할 수 있습니다.
- 통신과 관련된 기본적인 추상화 혜택을 받고자 활용합니다.
- interceptor로 인가과 갱신처리 합니다.
- loader를 통해 route protect을 적용할 수 있습니다.
- loader에서 prefetch를 사용하고 request-waterfall로 보이는 로딩 스피너를 숨길 수 있습니다.
- 통신 상태를 활용할 수 있습니다.
- 통신 결과를 캐싱할 수 있습니다.
- 통신은 비동기고 화면은 동기적으로 동작시킬 수 있습니다.
- 캐시 키를 통해 전역으로 상태를 공유할 수 있습니다.
- 서버 통신과 무관한 상태관리를 아주 간단하게 할 수 있습니다.
- 스타일링 자원을 간단하게 공유할 수 있습니다.
- 스타일링을 보수하기 수월합니다.
- Spinner를 다루기 상당히 간단합니다. storybook 문서를 보고 원하는대로 만들고 붙이면 됩니다.
- 코드를 읽을 때 제일 중요한 함수를 최상단에 위치시킵니다.
- function 키워드는 호이스팅(hoisting)의 장점을 활용합니다.
const SubComponent = () => {
return <div>Not Important</div>;
};
const Component = () => {
return <SubComponent />;
};
중요한 것을 미괄식으로 표현합니다.
function Component() {
return <SubComponent />;
}
function SubComponent() {
return <div>Not Important</div>;
}
호이스팅이 중요한 것을 두괄식으로 표현할 수 있게 해줍니다.
function useSomething() {
const doSomething = useCallback(() => {
// do something
}, []);
const handleSomething = () => {
// do something else
};
return { somethingValue, handleSomething };
}
관심사에 맞지 않은 hook과 handler가 섞이고 결합되는 방지하기 위해 영역을 구분합니다.
function Component() {
const [inputVal, setInputVal] = useState('');
const changeInputVal = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputVal(e.target.value);
};
const inputRef = useRef<HTMLInputElement>(null);
const focusInput = () => {
inputRef.current?.focus();
};
return <input value={inputVal} onChange={changeInputVal} ref={inputRef} />;
}
hook과 handler가 섞여 있습니다. 지금은 직관적이지만 나중에 useEffect
, 조건문, hook에 handler 대입하는 것처럼 로직이 추가되고 섞이면 관심사에 맞는 코드를 구분하기 어려워질 수 있습니다.
function Component() {
// hook 영역 시작 -------------------------------------------------------------
const [inputVal, setInputVal] = useState('');
const inputRef = useRef<HTMLInputElement>(null);
// hook 영역 종료 & handler 영역 시작 -------------------------------------------
const changeInputVal = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputVal(e.target.value);
};
const focusInput = () => {
inputRef.current?.focus();
};
// handler 영역 종료 & JSX 영역 시작 --------------------------------------------
return <input value={inputVal} onChange={changeInputVal} ref={inputRef} />;
// JSX 영역 종료 = ------------------------------------------------------------
}
JSX에 주입하고 이벤트를 처리할 함수와 hook이라는 관심사를 분리합니다.
라이프 사이클이외 관심사에 맞지 않은 handler 함수를 주입할지도 모릅니다.
function Component() {
const { handleBar } = useFoo('');
const { handleQux } = useBaz('');
useEffect(() => {
handleBar();
handleQux();
}, []);
return <NotImportant />;
}
라이프사이클에 각각 다른 관심사가 하나로 결합되었습니다. 하나의 함수는 update에 구독해야 하고 다른 함수는 mount시점만 필요하면 분리가 필요합니다.
function Component() {
useCorge();
useGrault();
return <NotImportant />;
}
function useCorge() {
const { handleBar } = useFoo('');
useEffect(() => {
handleBar();
}, []);
return {};
}
function useGrault() {
const { handleQux, graply } = useBaz('');
useEffect(() => {
handleQux();
}, [graply]);
return {};
}
useEffect 사용한다점 자체로 하위 계층구조로 간주합니다. 서로 구독해야 하는 라이프사이클을 독립적으로 구분합니다.
- 당일 올린 PR은 당일 Merge하지 않습니다.
- 1.1.0부터는 Merge 수량에 제한은 없습니다.
- 1.0.0까지는 하루 1개로 제한했었습니다.
- PR에 대한 리뷰는 다음날 진행합니다.
- 다음날 확인하면 코드를 읽을 때 환기된 상태로 PR을 검토할 수 있습니다.
- PR 리뷰에 대해서 대응여부는 재량이지만 응답은 필수입니다.
- PR 템플릿을 준수합니다.
- PR을 올릴 때는 최대한 이미 혹은 영상을 활용하도록 합니다.
- 해결하는 비즈니스 문제 혹은 엔지니어링 문제도 자세히 기술하도록 합니다.
yarn
yarn dev
yarn test
yarn build