영상의 40분 30초부터 1시간 12분까지의 내용을 다룬다.
목차.
TailwindCSS 셋업
vite로 생성한 프로젝트는 node_modules이 없으므로 npm install을 먼저 해주어야한다.
App.css 삭제, index.css 내용 비우기.
https://tailwindcss.com/docs/guides/vite
Vite에서 Tailwind CSS 사용하기.
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {},
},
plugins: [],
};
/* src/index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
vscode 사용 시 @tailwind에 에러가 발생한다면 익스텐션에서 PostCSS Language Support를 설치하면 된다.
참고 - https://velog.io/@jinsunkimdev/React에서-TailwindCSS-사용-시-warning-Unknown-at-rule-tailwind-css
import { useState } from "react";
function App() {
const [count, setCount] = useState(0);
return (
<>
<div className="bg-red-500">hello</div>
</>
);
}
export default App;
vscode에서 리액트에 tailwindcss 자동완성이 안되어서 되도록 설정해려면
익스텐션에서 Tailwind CSS IntelliSense를 설치하고,
ctrl + shift + p로 커맨드 팔레드를 열어서 Open User Setting(Json)을 찾은 후에,
{
...
"files.associations": {
...
"*.css": "tailwindcss"
},
"editor.quickSuggestions": {
"strings": true
}
...
}
이렇게 추가하면 된다. 나의 경우 files.associations가 이미 존재하여 ctrl + f로 찾기 후에 css를 추가하고, editor.quickSuggestions는 존재하지않았다.
npm run dev로 로컬서버 실행 후, 글자에 배경색으로 빨갛게 되어있다면 tailwindcss가 잘 적용되었음을 알 수 있다.
src 폴더에 pages 폴더를 생성 후 Feed.tsx 파일을 만든 뒤에, vscode에 리액트 관련 익스텐션이 잘 세팅되어있다면, rfce를 입력하면 리액트 자동완성이 뜬다.
강의 영상에선 Simple React Snippets을 쓰는데, 난 안쓰고 있다.
나는 ES7+ React/Redux/React-Native snippets 이걸 쓰고 있는데 여기서 자동완성 기능을 제공한다.
// src/pages/Feed.tsx
import React from "react";
function Feed() {
return <div>Feed</div>;
}
export default Feed;
우선 기본뼈대로 Feed, Post, Profile, Upload의 tsx 파일을 pages 폴더에 만든다.
react-router-dom 셋업
npm i react-router-dom
// src/main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
import {
RouterProvider,
createBrowserRouter,
} from "react-router-dom";
import Feed from "./pages/Feed.tsx";
import Upload from "./pages/Upload";
import Profile from "./pages/Profile.tsx";
import Post from "./pages/Post.tsx";
const router = createBrowserRouter([
{
path: "/feed",
element: <Feed />,
},
{
path: "/upload",
element: <Upload />,
},
{
path: "/profile/:id",
element: <Profile />,
},
{
path: "/post/:id",
element: <Post />,
},
]);
ReactDOM.createRoot(
document.getElementById("root")!
).render(
<React.StrictMode>
<RouterProvider router={router} />
<App />
</React.StrictMode>
);
로컬 서비 실행 후 /upload, /profile/1, /post/1에 들어가 react-router-dom이 잘 세팅되었는지 확인.
상태관리 Zustand
zustand를 잠깐 사용해보았는데, 개인적으론 redux와 recoil보다 훨씬 간단하였다. apollo client의 상태관리, svelte의 상태관리와 함께 사용하기 편하다.
zustand는 리액트의 컨텍스트 api를 기반으로 한다는데, 이게 리덕스와 리코일과 다르게 가지는 장점이라 한다.
npm i zustand
// src/stores/generalStore.ts
import { create } from "zustand";
import { persist, devtools } from "zustand/middleware";
// 스토어 상태를 정의하는 인터페이스.
export interface GeneralStore {
isLoginOpen: boolean;
isEditProfileOpen: boolean;
selectedPosts: null;
ids: null;
posts: null;
}
// 스토어에서 사용할 동작을 정의하는 인터페이스.
export interface GeneralActions {
setLoginIsOpen: (isLoginOpen: boolean) => void;
setIsEditProfileOpen: () => void;
}
// Zustand 스토어를 생성하고 설정.
export const useGeneralStore = create<
GeneralStore & GeneralActions
>()(
// 개발자 도구를 사용하기 위한 미들웨어.
devtools(
// 로컬 스토리지에 상태를 저장하고 복원하기 위한 미들웨어.
persist(
(set) => ({
isLoginOpen: false,
isEditProfileOpen: false,
selectedPosts: null,
ids: null,
posts: null,
setLoginIsOpen: (isLoginOpen) =>
set({ isLoginOpen }),
setIsEditProfileOpen: () => {
return set((state) => ({
isEditProfileOpen: !state.isEditProfileOpen,
}));
},
}),
{
// 로컬 스토리지에 저장될 키 이름.
name: "general-store",
}
)
)
);
// src/stores/userStore.ts
import { create } from "zustand";
import { persist, devtools } from "zustand/middleware";
export interface User {
id?: string;
fullname: string;
email?: string;
bio?: string;
image?: string;
}
export interface UserActions {
setUser: (user: User) => void;
logout: () => void;
}
export const useUserStore = create<User & UserActions>()(
devtools(
persist(
(set) => ({
id: "",
fullname: "",
email: "",
bio: "",
image: "",
setUser: (user) => set(user),
logout: () => {
set({
id: "",
fullname: "",
email: "",
bio: "",
image: "",
});
},
}),
{
name: "user-storage",
}
)
)
);
GraphQL, Apollo 셋업
npm i graphql @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo apollo-upload-client @types/apollo-upload-client @apollo/client
@graphql-codegen/cli: GraphQL 코드 생성 도구로, GraphQL 스키마를 기반으로 TypeScript 코드를 자동으로 생성할 수 있게 해줍니다.
@graphql-codegen/typescript: GraphQL 코드 생성 도구를 TypeScript용으로 사용할 수 있게 해주는 플러그인입니다.
@graphql-codegen/typescript-operations: GraphQL 연산과 쿼리를 TypeScript 코드로 생성하는 데 사용되는 플러그인입니다.
@graphql-codegen/typescript-react-apollo: Apollo Client와 함께 사용하는 React 애플리케이션에서 TypeScript 코드를 생성하는 플러그인입니다.
apollo-upload-client: 파일 업로드와 관련된 GraphQL 요청을 처리하는 Apollo Client용 라이브러리입니다.
// src/utils/apolloClient.ts
import {
ApolloClient,
InMemoryCache,
NormalizedCacheObject,
gql,
Observable,
ApolloLink,
} from "@apollo/client";
// The Observable class in @apollo/client has the following functionality that we need:
// - subscribe() method to subscribe to updates
// - unsubscribe() method to unsubscribe from updates
// - next() method to send updates to subscribers
// - error() method to send errors to subscribers
// - complete() method to send completion messages to subscribers
import { createUploadLink } from "apollo-upload-client";
import { onError } from "@apollo/client/link/error";
// 새로운 액세스 토큰을 얻는 비동기 함수
async function refreshToken(
client: ApolloClient<NormalizedCacheObject>
) {
try {
// GraphQL 뮤테이션을 사용하여 새로운 액세스 토큰을 요청
const { data } = await client.mutate({
mutation: gql`
mutation RefreshToken {
refreshToken
}
`,
});
// 새로운 액세스 토큰을 받아옴
const newAccessToken = data?.refreshToken;
console.trace("newAccessToken", newAccessToken);
if (!newAccessToken) {
throw new Error("New access token not received.");
}
// 새로운 액세스 토큰을 로컬 스토리지에 저장
localStorage.setItem("accessToken", newAccessToken);
// 액세스 토큰을 Bearer 토큰 형식으로 반환
return `Bearer ${newAccessToken}`;
} catch (err) {
console.trace(err);
throw new Error("Error getting a new access token.");
}
}
// 토큰 재시도 횟수 및 최대 재시도 횟수 설정
let retryCount = 0;
const maxRetry = 3;
// 오류 처리를 담당하는 Apollo Link
const errorLink = onError(
({ graphQLErrors, operation, forward }) => {
if (graphQLErrors) {
for (const err of graphQLErrors) {
// "UNAUTHENTICATED" 오류와 재시도 횟수가 최대 재시도 횟수보다 적을 경우
if (
err.extensions.code === "UNAUTHENTICATED" &&
retryCount < maxRetry
) {
retryCount++;
return new Observable((observer) => {
// Promise를 사용하지 않고 Observable을 사용하는 이유는
// Promise는 하나의 값으로 resolve되는 반면,
// Observable은 하나의 값이 아니라 여러 값을 처리할 수 있는 데이터 스트림을
// 나타낸다는 점 때문이다.
// 하나의 값이 아니라 여러 값을 다루어야 하므로 Observable을 사용하고,
// 여러 번 시도해야 할 수도 있기 때문에 단순한 단일 값이 아니라 여러 값을 처리할 수 있어야 한다.
console.trace("observer", observer);
// 새로운 액세스 토큰을 얻어오고, 헤더에 추가한 후 원래 요청 다시 보내기
refreshToken(client)
.then((token) => {
console.trace("token", token);
operation.setContext(
(previousContext: any) => ({
headers: {
...previousContext.headers,
authorization: token,
},
})
);
const forward$ = forward(operation);
forward$.subscribe(observer);
})
.catch((error) => observer.error(error));
});
}
}
}
}
);
// 파일 업로드를 지원하는 Apollo Link
const uploadLink = createUploadLink({
uri: "http://localhost:3000/graphql", // GraphQL 서버 URI
credentials: "include",
headers: {
"apollo-require-preflight": "true",
},
});
// Apollo Client 인스턴스 생성
export const client = new ApolloClient({
uri: "http://localhost:3000/graphql", // GraphQL 서버 URI
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
getCommentsByPostId: {
merge(existing, incoming) {
return incoming;
},
},
},
},
},
}),
credentials: "include",
headers: {
"Content-Type": "application/json",
},
// Apollo Link를 사용하여 오류 처리 및 파일 업로드 기능 추가
link: ApolloLink.from([errorLink, uploadLink]),
});
graphql 코드 제너레이터에 쓰일 쿼리 작성. 파일 이름은 소문자로 해야한다. 영상에선 대문자로 하는데, 후에 리액트 컴포넌트에도 로그인, 레지스터를 대문자로 만들고 쿼리 파일들을 소문자로 변경한다.
// src/graphql/mutations/login.ts
import { gql } from "@apollo/client";
export const LOGIN_USER = gql`
mutation LoginUser($email: String!, $password: String!) {
login(
loginInput: { email: $email, password: $password }
) {
user {
email
id
fullname
}
}
}
`;
// src/graphql/mutations/register.ts
import { gql } from "@apollo/client";
export const REGISTER_USER = gql`
mutation RegisterUser(
$fullname: String!
$email: String!
$password: String!
$confirmPassword: String!
) {
register(
registerInput: {
fullname: $fullname
email: $email
password: $password
confirmPassword: $confirmPassword
}
) {
user {
id
fullname
email
}
}
}
`;
// src/graphql/mutations/logout.ts
import { gql } from "@apollo/client";
export const LOGOUT_USER = gql`
mutation LogoutUser {
logout
}
`;
graphql 코드 제너레이터 셋업
// codegen.ts
import { CodegenConfig } from "@graphql-codegen/cli";
const config: CodegenConfig = {
schema: "http://localhost:3000/graphql",
documents: ["src/graphql/**/*.ts"],
ignoreNoDocuments: true, // for better experience with the watcher
generates: {
"./src/gql/": {
preset: "client",
plugins: [
"typescript",
"typescript-operations",
"typescript-react-apollo",
],
config: {
withHooks: true,
withHOC: false,
withComponent: false,
},
},
},
};
export default config;
https://the-guild.dev/graphql/codegen/docs/guides/react-vue - graphql code generator의 react 가이드.
npx graphql-codegen --watch
에러가 발생해서 ts-node와 @graphql-codegen/client-preset을 설치하니 해결되었다.
npm i ts-node @graphql-codegen/client-preset
Failed to load schema from http://localhost:3000/graphql 에러가 떴다. 백엔드 서버를 실행해야한다.
백엔드 서버를 실행하려면 에러가 발생하는데, 다음 글에서 작성.
'NodeJS' 카테고리의 다른 글
풀스택 틱톡 클론 따라하기 - 4. AuthModal - Register (1) | 2023.11.20 |
---|---|
풀스택 틱톡 클론 따라하기 - 3. Resolver, ProtectedRoutes (1) | 2023.11.20 |
풀스택 틱톡 클론 따라하기 - 1. 백엔드 셋업 (0) | 2023.11.05 |
타입스크립트 시작하기 (1) | 2023.01.02 |
타입스크립트에서 세션에 커스텀 속성 사용하기 (0) | 2022.03.03 |