1. yarn (install)
2. yarn run dev
- νκ²½λ³μ νμΌμ μ¬λ or μ§μ
jellybrown
μκ² λ¬Όμ΄λ΄μ£ΌμΈμ π
- νλ‘μ νΈλ νμ¬ dev νκ²½μμλ§ κ°λ°μ€μ λλ€. (dev = master λΈλμΉ)
- λ³ΈμΈμ κΉν κ²μ μΌλ‘ fork
- λλλΉ΅μ§λ λ ν¬ (upstream) - λ³ΈμΈ λ ν¬ (origin) λ‘ μ€μ ν ν κ΄λ¦¬ν΄μ£ΌμΈμ.
- κ°λ° ν originμμ upstream(master)μΌλ‘ PRμ λ 립λλ€.
- μμ λ‘κ² μ½λ리뷰, ꡬ쑰κ°μ λ± λ§μν΄μ£Όμ λ λ©λλ€!
- Emotion
- Client: Redux Toolkit
- Server: React Query
- Eslint (airbnb, μΉ μ κ·Όμ±, hooks)
- Prettier [WebStorm μ€μ κ°μ΄λ]
- React Testing Library
- Jest
βββ apis
β βββ axios # instance μ μΈ, interceptor λ‘μ§
β βββ bakery # api λΆλ¦¬
β βββ bakery.ts # μ¬μ©λλ api class
β βββ bakeryClient.ts # api classμ μ£Όμ
ν΄μ μ¬μ©νλ client
β βββ useBakery.ts # Query hooks
β βββ ...
βββ components
β βββ __tests__
β βββ SharedTable.test.tsx # Shared (곡ν΅) μ»΄ν¬λνΈ ν
μ€νΈ
β βββ LoginForm.test.tsx # μΌλ° μ»΄ν¬λνΈ ν
μ€νΈ
β βββ Auth # μΈμ¦ κ΄λ ¨ μ»΄ν¬λνΈ
β βββ Shared # κ³΅ν΅ μ»΄ν¬λνΈ
β βββ ...
βββ constants # μμκ΄λ¦¬
βββ containers # νλ©΄μ ν΄λΉνλ μ»΄ν¬λνΈλ₯Ό λ΄κ³ μλ 컨ν
μ΄λ. νμν΄λλ νλ©΄ κΈ°μ€ λΆλ¦¬
βββ context # μ μ/μ»΄ν¬λνΈμν κ΄λ¦¬κ° νμν κ²½μ° μ¬μ©
βββ hooks # Custom hooks μ μ
βββ routes
β βββ loader.ts # λΌμ°νΈ μ§μ
μ μ€νμ΄ νμν κ²½μ° μ¬μ©
β βββ ...
βββ store # νμ¬ bakery formμ λν μνλ₯Ό μ μ₯νκ³ μμΌλ, λ€λ₯Έκ±Έλ‘ λ³κ²½μμ
βββ styles
β βββ common # μμ£Ό μ¬μ©λλ μ€νμΌ
β βββ global.tsx # css μ΄κΈ°ν λ° κΈ°λ³Έμ€μ
β βββ theme.ts # νλ‘μ νΈ ν
λ§ μμ/ν¬κΈ° λ±
β βββ ...
βββ tests # ν
μ€νΈμ νμν mock, function ..
βββ utils # λ
립μ μΈ ν¨μλ€ μ μΈ
μΌλΆ 컨벀μ
/λ°©μλ€μ κ°μ ν μμ μ΄κ³ , μ견 νμν©λλ€ π₯Ί
- νμ΄μ§μ λ€μ΄κ°λ μ΅μμ μ»΄ν¬λνΈλ₯Ό Containers μ λ§λλλ€.
βββ container
β βββ Bakeries
β βββ Login
β βββ LoginContainer.tsx # {νμ΄μ§λͺ
μΉ}Container
β βββ index.ts # export components
β βββ ...
- container μμ λ€μ΄κ°λ μ»΄ν¬λνΈλ€μ components μμμ λ§λ€κ³ , λΆλ¬μ΅λλ€.
- μ»΄ν¬λνΈμ λ€μ΄κ°λ λͺ¨λ λ‘μ§μ containerμ μμ±ν΄μ€λλ€.
// containers/LoginContainer.tsx
export const LoginContainer = () => {
const navigate = useNavigate();
const {
login: { mutate: login, error },
} = useLogin();
const { activate: isRemembered, onActive: onActiveRemember, onInactive: onInactiveRemeber, onToggleActive: onToggleRemember } = useToggle();
const { form, onChangeForm, onSetForm } = useForm<LoginForm>(initialForm);
useEffect(() => {
// ...
return (
<Container>
<Wrapper>
<Logo />
<LoginForm form={form} onChangeForm={onChangeForm} isRemembered={isRemembered} onToggleRemember={onToggleRemember} />
<Button type={'orange'} text={'λ‘κ·ΈμΈ'} onClickBtn={() => onSubmit()} />
</Wrapper>
</Container>
);
- λλΆλΆμ ν΄λꡬ쑰λ μλμ κ°μ΄ ν΄λκ° λλμ΄μ Έμκ³ , ν΄λλ§λ€ index.tsκ° μμ΅λλ€.
βββ Shared
β βββ Button
β βββ Button.tsx
β βββ index.ts
β βββ Loading
β βββ Loading.tsx
β βββ TableLoading.tsx
β βββ index.ts
β βββ index.ts
β βββ ...
- μ΄λ λͺ¨λμ importν λ importλ¬Έμ μ€ μλ₯Ό μ€μ΄κΈ° μν¨μ λλ€.
import { Button, Input, Preview, SelectBox, SelectOption } from '@/components/Shared';
- νμ§μμ κ²½μ°
import { Button } from '@/components/Shared/Button';
import { Input } from '@/components/Shared/Input';
import { Preview } from '@/components/Shared/Preview';
import { SelectBox } from '@/components/Shared/SelectBox';
import { SelectOption } from '@/components/Shared/SelectOption';
- μ»΄ν¬λνΈ, ν¨μ λ±λ± λλΆλΆμ κ²½μ°, μ μΈ μμͺ½μ export ν΄μ€λλ€. (Named Export)
// β export default BakeriesContainer;
export const BakeriesContainer = () => {
}
- index.tsμμ λͺ¨λμ λ΄λ³΄λΌ λλ * λ‘ μ 체λ₯Ό λ΄λ³΄λ λλ€.
// β export { SkeletonCell } from './SkeletonCell';
export * from './SkeletonCell';
- νμ μ export ν΄μ€ κ²½μ° export typeμ μ¬μ©ν΄μ£ΌμΈμ.
export type { TableHeader, TableCell, TableProps } from './types';
Named Export, Default Export μ°Έκ³ μλ£
- μ€ μλΉμ€μ μ¬μ©λλ apiλ Context APIλ₯Ό μ΄μ©ν©λλ€.
- ν μ€νΈμ μ©μ΄νκ² DI(Dependency Injection, μμ‘΄μ± μ£Όμ )λ₯Ό μ΄μ©νμ¬ κ΅¬νν©λλ€.
- μ¬μ©λλ api, context ν΄λμ ꡬ쑰λ λ€μκ³Ό κ°μ΅λλ€.
βββ api
β βββ bakery
β βββ bakery.ts
β βββ bakeryClient.ts
β βββ useBakeries..ts
β βββ useBakery.ts
β βββ types.ts
β βββ index.ts
β βββ ...
βββ context
β βββ bakery
β βββ BakeryApiContext.tsx
β βββ BakeryApiProvider.tsx
β βββ index.ts
β βββ ...
β βββ ApiProvider.ts
- clientλ₯Ό μ£Όμ λ°μ api classμ client classλ₯Ό μ μΈν©λλ€.
// api/bakery/kbakery.ts
export class Bakery {
constructor(public client: BakeryApiClient) {}
async getItem({ bakeryId }: { bakeryId: number }) {
const item = await this.client.getItem({ bakeryId });
return item;
}
async createItem({ payload }: CreateUpdateBakeryPayload) {
await this.client.createItem({ payload });
}
// apis/bakery/bakeryClient.ts
export class BakeryClient implements BakeryApiClient {
async getItem({ bakeryId }: { bakeryId: number }) {
const resp = await fetcher.get<BakeryDetailEntity>(`bakery/${bakeryId}`);
return resp.data;
}
async createItem({ payload }: CreateUpdateBakeryPayload) {
await fetcher.post('bakery', payload, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
}
- ν΄λΉ apiμ contextμ providerλ₯Ό λ§λ€κ³ , ApiProviderμ μΆκ°ν΄μ€λλ€.
// context/bakery/BakeryApiContext.tsx
export const BakeryApiContext = createContext<{ bakery: Bakery | null }>({ bakery: null });
export const useBakeryApi = () => {
return useContext(BakeryApiContext);
};
// context/bakery/BakeryApiProvider.tsx
const client = new BakeryClient();
const bakery = new Bakery(client);
export const BakeryApiProvider = ({ children }: { children: ReactNode }) => {
return <BakeryApiContext.Provider value={{ bakery }}>{children}</BakeryApiContext.Provider>;
};
// context/ApiProvider.tsx
export const ApiProvider = ({ children }: { children: ReactNode }) => {
return (
<BakeryApiProvider>
<BakeryReportApiProvider>{children}</BakeryReportApiProvider>
</BakeryApiProvider>
);
};
- React Queryλ₯Ό μ΄μ©ν΄μ Query hookμ μ μΈν λ λΆλ¬μμ μ¬μ©ν©λλ€.
// apis/bakery/useBakery.ts
export const useBakery = ({ bakeryId }: { bakeryId: number }) => {
const { bakery } = useBakeryApi();
const queryClient = useQueryClient();
if (!bakery) {
throw new Error('bakeryApiλ₯Ό νμΈν΄μ£ΌμΈμ.');
}
const bakeryQuery = useQuery(['bakery', { bakeryId }], () => bakery.getItem({ bakeryId }), {
enabled: !isNaN(bakeryId),
});
const addBakery = useMutation(bakery.createItem, {
onSuccess: () => queryClient.invalidateQueries('getBakeries'),
});
- νλ©΄μμ apiλ₯Ό μ΄μ©ν λλ Query hookμ μ΄μ©ν©λλ€.
// containers/bakeryDetail/BakeryDetailContainers.tsx
const {
bakeryQuery: { data: bakery },
addBakery,
editBakery,
} = useBakery({ bakeryId: Number(bakeryId) });
// ...
const onCreateForm = (payload: FormData) => {
addBakery.mutate(
// ...
{
"test": "jest --watchAll",
"test:coverage": "jest --coverage --watchAll"
}