๐ TaskTracker - ํ๊ณ
๐ฏ ํ๋ก์ ํธ ๊ฐ์ ๋ณธ ํ๋ก์ ํธ๊ฐ ์งํฅํ๋ ๋ฐ๋ ๋ค์๊ณผ ๊ฐ์๋ค. ํ๋ก์ ํธ์ ํ์์ฑ์ ๋ณธ์ธ ์ค์ค๋ก ๋ฉ๋ํ ์ ์์ ๊ฒ Firebase๋ฅผ ํตํ Authentication๊ณผ CRUD์ ์ ๊ณผ์ ์ ๊ฒฝํํ ๊ฒ Redux-Toolkit์ ํตํด ์ ์ญ ์ํ ๊ด๋ฆฌ๋ฅผ ๊ฒฝํํ ๊ฒ >
https://tasktracker-livid.vercel.app/home
๐ฏ ํ๋ก์ ํธ ๊ฐ์
๋ณธ ํ๋ก์ ํธ๊ฐ ์งํฅํ๋ ๋ฐ๋ ๋ค์๊ณผ ๊ฐ์๋ค.
1. ํ๋ก์ ํธ์ ํ์์ฑ์ ๋ณธ์ธ ์ค์ค๋ก ๋ฉ๋ํ ์ ์์ ๊ฒ 2. Firebase๋ฅผ ํตํ Authentication๊ณผ CRUD์ ์ ๊ณผ์ ์ ๊ฒฝํํ ๊ฒ 3. Redux-Toolkit์ ํตํด ์ ์ญ ์ํ ๊ด๋ฆฌ๋ฅผ ๊ฒฝํํ ๊ฒ
โ ํ๋ก์ ํธ์ ํ์์ฑ
๋ถํธํ๋ค. ๊ณํ์ ์ธ์ฐ๋ฉด ์งํค์ง ์๊ณ , ์ธ์ฐ์ง ์์ผ๋ฉด ๋ถ์ํ๋ค. ๊ทธ๋ฆฌ๊ณ ๋ชจ๋ ๊ฒ์ ์์ธกํ ๋งํผ ๋๋ํ์ง ์์๋ค. ๋งค๋ฒ ์ํํ๋ ค๋ ์์ ์ ๋์ด๋๋ ์์ดํ๊ณ , ์์ธกํ ์ ์๋ ๋ฌด์ธ๊ฐ๋ฅผ ์์ธกํ๋ ค ํ๊ธฐ์ ๋น์ฐํ ์คํจํ๋ฉด์, ๋ฌด์์ธ๊ฐ ์๋ชป๋๋ค๋ ์๊ฐ์ด ๋ค์๋ค.
์์ธ๊ณผ ๊ฒฐ๊ณผ๊ฐ ๋ฐ๋๋ ์๊ฐ์ด ๋ถ๋ช ํ ์๋ค. ๊ทธ๋ฌ๋๊น, ๊ณํ๊ณผ ์คํ์ ๋ฌธ์ ๊ฐ ์์ด์ ๋ถ์ํด์ง๋ ๊ฒ์ด ์๋๋ผ, ๋ถ์ํ๊ธฐ ๋๋ฌธ์ ๊ณํ๊ณผ ์คํ์ด ๋ง๊ฐ์ง๋ ์ํฉ์ด ์๋ค๋ ๊ฒ์ด๋ค. ๋ถ์์ด ์์ธ์ด๋ผ๋ฉด ๊ทธ๊ฒ์ ๊ฒฝ๊ฐํ๋ ค๋ ์ชฝ์ด ํด๊ฒฐ์ ๊ฐ๊น๊ฒ ๋ค.
๋ฝ๋ชจ๋๋ก ๊ธฐ๋ฒ์ ๊ฝค๋ ์ ๋ช ํ ์๊ฐ๊ด๋ฆฌ ๋ฐฉ๋ฒ๋ก ์ค ํ๋์๊ณ , ๋ฒํผ์ ํด๋ฆญํ๋ ์๊ฐ ๋ด๊ฐ ์ค์ ํ Task์ ์๊ฐ์ ์จ์ ํ ์ง์คํ๊ฒ ๋๋ค๋ ์ ์ด ๋ถ์ ๊ด๋ฆฌ์ ์๋นํ ์ ์ฉํ๋ค๋ ์๊ฐ์ด ๋ค์๋ค.
ํ๋ก์ ํธ๋ ์ฌ์ค์ ๊ป๋ฐ๊ธฐ์ ๋ถ๊ณผํ๋ค. ํ๋ก์ ํธ๋ผ๋ ๋ฐ์ด๋๋ฆฌ ์์์ ์ด๋ ํ ๊ธฐ์ ์ ์ต๋ํ๊ณ ์๋ จํ์ผ๋ฉฐ ๋ฌด์จ ์ด์ผ๊ธฐ๋ฅผ ๋ด์๋์ง๊ฐ ํจ์ฌ ์ค์ํ๊ณ , ์ด๋ฌํ ์์ ์ ์ฌ์ ํ ์ด๋ ต์ง๋ง, ๊ณ ๋ฏผ์ ์ด๋์ด ํ๋ก์ ํธ์ด๊ธฐ ๋๋ฌธ์ ๊ณ ๋ฏผ์ด ์ ์ ๋์ง ์์ ํ๋ก์ ํธ๋ฅผ ์ํํ ๊ฒ์ด๋ผ๋ฉด, ๊ทธ๋ฅ ๋๊ฐ์ ๋ ธ๋ ๊ฒ ํ๋ณต์ ๋ ์ ๋ฆฌํ ๊ฒ์ด๋ผ๋ ๊ฒ์ด ๋ด ์๊ฐ์ด์๋ค. ์ญ์ค์ ์ด๋ค.
โ Authentication
1. createUserWithEmailAndPassword(ํ์ ๊ฐ์ ) โ๏ธ
// ํ์๊ฐ์
์ ์ฒ๋ฆฌํ๋ ํจ์
const handleSignUp = async (e) => {
e.preventDefault();
if (Object.values(errors).some((error) => error)) {
alert("Please check your information again.");
return;
}
await dispatch(
signupUser({
email: userInfo?.email,
password: userInfo?.password,
imgFile,
displayName: userInfo?.username,
})
).unwrap();
alert("Sign up successful!");
navigate("/signin");
};export const signupUser = createAsyncThunk(
"user/signupUser",
async (userInfo) => {
try {
// Firebase๋ฅผ ํตํด, ์ด๋ฉ์ผ๊ณผ ๋น๋ฐ๋ฒํธ๋ก ์ฌ์ฉ์๋ฅผ ์์ฑ
await createUserWithEmailAndPassword(
auth,
userInfo?.email,
userInfo?.password
);
// ํ์ฌ ์ฌ์ฉ์๊ฐ ์๋ ๊ฒฝ์ฐ(=firebase์ ํ์ ๋ฑ๋ก์ด ๋ ๊ฒฝ์ฐ)
if (auth.currentUser) {
// ๋์์ ์ด๋ฏธ์ง ํ์ผ์ด ์๋ ๊ฒฝ์ฐ
if (userInfo?.imgFile) {
// firebase์ ์ด๋ฏธ์ง๋ฅผ ์
๋ก๋
const storageRef = ref(
storage,
new Date().getTime() + userInfo?.imgFile?.name
);
const uploadTask = uploadBytesResumable(
storageRef,
userInfo?.imgFile
);
uploadTask.on(
"state_changed",
// ์
๋ก๋ ์ํ๊ฐ ๋ณ๊ฒฝ๋ ๋ ์คํ๋ ์ฝ๋ฐฑ ํจ์
(snapshot) => {
// ์
๋ก๋ ์งํ๋ฅ ์ ๊ณ์ฐํ๋ ๋ก์ง
const progress =
(snapshot.bytesTransferred / snapshot.totalBytes) * 100;
console.log("Upload is " + progress + "% done");
switch (snapshot.state) {
case "paused":
console.log("Upload is paused");
break;
case "running":
console.log("Upload is running");
break;
}
},
// ์
๋ก๋ ์ค ์๋ฌ๊ฐ ๋ฐ์ํ ๊ฒฝ์ฐ
(error) => {
alert("Failed to upload image. Please try again later.");
},
// ์
๋ก๋๊ฐ ์๋ฃ๋ ๊ฒฝ์ฐ
() => {
// ์
๋ก๋๋ ํ์ผ์ ๋ค์ด๋ก๋ URL์ ๊ฐ์ ธ์ด
getDownloadURL(uploadTask.snapshot.ref).then(
async (downloadURL) => {
// ์ฌ์ฉ์ ํ๋กํ์ ์
๋ฐ์ดํธ
await updateProfile(auth.currentUser, {
displayName: userInfo?.displayName,
photoURL: downloadURL,
});
}
);
}
);
}
// ์ด๋ฏธ์ง ํ์ผ์ด ์๋ ๊ฒฝ์ฐ
else {
await updateProfile(auth.currentUser, {
displayName: userInfo?.displayName,
photoURL: "",
});
}
}
} catch (error) {
// ์์ธ๊ฐ ๋ฐ์ํ ๊ฒฝ์ฐ
switch (error.code) {
case "auth/email-already-in-use":
alert("Email already in use.");
break;
case "auth/weak-password":
alert("Password should be at least 6 characters.");
break;
case "auth/network-request-failed":
alert("Network request failed.");
break;
case "auth/invalid-email":
alert("Invalid email format.");
break;
case "auth/internal-error":
alert("Internal error.");
break;
default:
alert("Sign up failed.");
}
}
}
);์ฝ๋๋ฅผ ์ฃผ์ ์ฃผ์ ์ค๋ช ํ๋ ๊ฒ์ ํฌ๊ฒ ์๋ฏธ๊ฐ ์์ ๊ฒ ๊ฐ๋ค. Firebase์์ ์ ๊ณตํ๋ createUserWithEmailAndPassword ํจ์์ ์ ์ ํ ๊ฐ์ ์ ๋ฌํ๋ฉด ํ์ ๊ฐ์ ์ ์๋ฃ๋๋ค.
๊ทธ๋ฐ๋ฐ ์ฌ๋ฃ๊ฐ ๋ฌด์์ธ์ง ๊ทธ ์์ฑ์ด ์ด๋ ํ์ง๋ ๋ช ํํ ์ธ์งํด์ผ ํ๋ค. ํฅํ์ React-Query๋ Supabase ๋ฑ ๋ค์ํ ๊ธฐ์ ์ ํ๋ก์ ํธ์ ์ ์ฉํ ์์ ์ธ๋ฐ, ๋ด๊ฐ ํ์ฉํ ์ ์๋ ๋ฐ์ดํฐ์๋ ๋ฌด์์ด ์์ผ๋ฉฐ, ๊ทธ ๋ฐ์ดํฐ๋ค์ด ๊ฐ์ง๋ ์์ฑ์ด ๋ฌด์์ธ์ง ์์ง ๋ชปํ๋ฉด, ์ ๋๋ก ๋ ์ฝ๋๋ฅผ ์์ฑํ ์ ์๋ค.
๋ผ์ง๊ณ ๊ธฐ๊ฐ ์ํ๋๋ฉด ํฉํ์ด๋๋ฅผ ๊ฑฐ์ณ ์๋ฏธ๋ ธ์ฐ์ผ๋ก ๋ฐ๋๋๋ฐ, ์ด๋ ํ์ํ ๊ฒ์ด ๋จ๋ฐฑ์ง ๋ถํด ํจ์์ธ 'ํ๋กํ ์์ '๊ณ , ์์ฐ์ ์ด ๋ฐํจ๋๋ ๋์ ์์ฑ๋๋ ํ๋กํ ์์ ๋ ์ผ์ข ์ ์ํ์ ์ญํ ์ ํ๊ธฐ์, ์ผ๋ฐ์ ์ผ๋ก ์๋๊ตญ์ ๊ฐ์ ์์ฐ์ ์ผ๋ก ํ๋ค.
์ฝ๋ฉ์ ์ง์์ด ์๋์ ์ผ๋ก ๋ง์, ๊น๋ค๋ก์ด ์๋(like ๋ฐฑ์ข ์ ์ ์๋)์๊ฒ ์์ฌ๋ฅผ ๋์ ํ๋ ๊ณผ์ ๊ณผ ๊ฐ์์, ๋ด๊ฐ ์ฌ์ฉํ๋ ์ฌ๋ฃ๋ฅผ ๋ช ํํ ์๊ณ ์์ด์ผ๋ง ํ๋ค.
2. signInWithEmailAndPassword(๋ก๊ทธ์ธ) โ๏ธ
// ๋ก๊ทธ์ธ์ ์ฒ๋ฆฌํ๋ ํจ์
const handleSignIn = async () => {
try {
await dispatch(
signInUser({ email: signinInfo?.email, password: signinInfo?.password })
).unwrap();
navigate("/home");
} catch (error) {
console.error(error);
}
};export const signInUser = createAsyncThunk(
"user/signInUser",
async (userInfo) => {
const userCredential = await signInWithEmailAndPassword(
auth,
userInfo?.email,
userInfo?.password
);
const result = {
uid: userCredential?.user?.uid,
displayName: userCredential?.user?.displayName,
email: userCredential?.user?.email,
};
localStorage.setItem("user", JSON.stringify(result));
return result;
}
);Firebase์์ ์ ๊ณตํ๋ ํจ์๋ฅผ ์ฌ์ฉํ๋ค๋ ์ ์ ์ ์ธํ๊ณ ํน์ง์ ์ธ ๊ฒ์, ์ ์ ์ ๋ณด๋ฅผ localstorage์ ์ ์ฅํ๋ค๋ ์ ์ด๋ค.
์ฌ๋ฌ ์ฅ๋จ์ ์ด ์๊ฒ ์ง๋ง, ๋ธ๋ผ์ฐ์ ์ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๋ฉด, ์ ์ ๊ฐ ํ์ด์ง๋ฅผ ์๋ก๊ณ ์นจํ๊ฑฐ๋ ๋ธ๋ผ์ฐ์ ๋ฅผ ๋ซ์๋ค๊ฐ ๋ค์ ์ด์ด๋, localstorage์ ์ ์ฅ๋ ๋ฐ์ดํฐ๋ ๊ทธ๋๋ก ์ ์ง๋๊ธฐ์, ๋ก๊ทธ์ธ ์ํ ์ ์ง ๋ฑ ์ธ์ ๊ด๋ จ ๊ธฐ๋ฅ์ ๊ตฌํํ๋ ๋ฐ ์ ์ฉํ๊ธฐ ๋๋ฌธ์ ์ฌ์ฉํ๋ค.
3. onAuthStateChanged(ํ์ ์ ์ง) โ๏ธ
useEffect(() => {
dispatch(checkLoginUser(JSON.parse(localStorage.getItem("user"))));
}, [dispatch]);// ์ ์ ์ ๋ณด๋ฅผ ์ค์ ํ๋ ๋น๋๊ธฐ ํจ์
export const checkLoginUser = createAsyncThunk(
"user/checkLoginUser",
async (user) => {
console.log(user);
const auth = getAuth();
onAuthStateChanged(auth, (user) => {
console.log(user);
localStorage.setItem("user", JSON.stringify(user));
});
return user;
}
);
Appbar์์๋ ํด๋น useEffect๋ฅผ ์คํํ๋ค. ์ฆ, Appbar๊ฐ ์ ์๋๋ ๋ชจ๋ ์์ญ์์ ๊ณ์ํด์ ์ ์ ์ ๋ณด๋ฅผ checkLoginUser ํจ์๋ก dispatch ํ๋ค๋ ๊ฒ์ด๋ค.
Firebase์์ ์ ๊ณตํ๋ onAuthStateChanged ํจ์๋ฅผ ํตํด ๊ณ์ํด์ ์ ์ ์ ๋ณด๋ฅผ ๊ฐฑ์ ๋ฐ ์ ์งํ ์ ์๊ฒ ๋๋ค.
โ CRUD
1. Add Task ๋ฒํผ โ๏ธ
๋์: ์ฌ์ฉ์๊ฐ ์ ๋ ฅํ ์๋ก์ด ํ์คํฌ ์ด๋ฆ์ ๋ฆฌ์คํธ์ ์ถ๊ฐํ๋ค. ์๋ฅผ ๋ค์ด, ์ฌ์ฉ์๊ฐ "์ด๋ํ๊ธฐ"๋ผ๋ ํ์คํฌ๋ฅผ ์ ๋ ฅํ๋ฉด, ์ด ํ์คํฌ๊ฐ ๋ฆฌ์คํธ์ ์ถ๊ฐ๋๋ค.
2. Go to Archive ๋ฒํผ โ๏ธ
๋์: ํ์ฌ ํ๋ฉด์์ ์์นด์ด๋ธ ํ์ด์ง๋ก ์ด๋ํ๋ค. ์ด๋ ํ์๋ ํ ํ์ด์ง์ ์๋ ๋ชจ๋ complete ํ์คํฌ๋ค์ ํ์ธํ ์ ์๋ค.
3. All Clear ๋ฒํผ โ๏ธ
๋์: ์์นด์ด๋ธ ํ์ด์ง์ ์๋ ๋ชจ๋ complete ํ์คํฌ๋ค์ ์ญ์ ํ๋ค. ๋จ, ํ ํ์ด์ง์ ์ถ๊ฐ๋ ๋ด์ฉ์ complete๊ฐ ์๋๊ธฐ์ ์ญ์ ๋์ง ์๋๋ค.
4. Go Home ๋ฒํผ โ๏ธ
๋์: ์์นด์ด๋ธ ํ์ด์ง์์ ํ ํ์ด์ง๋ก ๋๋์๊ฐ๋ค. ์ด๋ ํ์๋ ๋ค์ ํ ํ์ด์ง์ ์ถ๊ฐ๋์ด ์๋ ํ์คํฌ๋ค๊ณผ ์์นด์ด๋ธ ๋ฒํผ์ ํ์ธํ ์ ์๋ค.
5. Start ๋ฒํผ โ๏ธ
๋์: ์ ํํ ํน์ ํ์คํฌ์ ํ์ด๋จธ๋ฅผ ์์ํ๋ค. ์๋ฅผ ๋ค์ด, "์ด๋ํ๊ธฐ" ํ์คํฌ์ ํ์ด๋จธ๋ฅผ ์์ํ๋ฉด, ํด๋น ํ์คํฌ๋ 'pending' ์ํ๋ก ํ์๋๋ฉฐ ์๊ฐ์ด ์งํ๋๋ค.
6. Pause ๋ฒํผ โ๏ธ
๋์: ํ์ฌ ์งํ ์ค์ธ ํ์คํฌ์ ํ์ด๋จธ๋ฅผ ์ผ์ ์ ์งํ๋ค. ์๋ฅผ ๋ค์ด, "์ด๋ํ๊ธฐ" ํ์คํฌ์ ํ์ด๋จธ๊ฐ ์ผ์ ์ ์ง๋๋ฉด, ํด๋น ํ์คํฌ๋ 'paused' ์ํ๋ก ๋ณ๊ฒฝ๋๋ฉฐ ์ ์ง๋ ๊ธฐ๋ก์ผ๋ก ํด๋น ํ์คํฌ๊ฐ ์๋ฒ์ ์ ์ฅ๋๋ค.
7. Delete ๋ฒํผ โ๏ธ
๋์: ์ ํํ ํน์ ํ์คํฌ๋ฅผ ๋ฆฌ์คํธ์์ ์ญ์ ํ๋ค. ์๋ฅผ ๋ค์ด, "์ด๋ํ๊ธฐ" ํ์คํฌ๋ฅผ ์ญ์ ํ๋ฉด, ์ด ํ์คํฌ๋ ๋ฆฌ์คํธ์์ ์ ๊ฑฐ๋์ด UI์์ ๋ณด์ด์ง ์๊ฒ ๋๋ค.
8. ๋ํ ์ฝ๋ โ๏ธ
const handleAddTask = () => {
if (!task.trim()) {
return alert("ํ์คํฌ๋ฅผ ์
๋ ฅํด์ฃผ์ธ์.");
}
const newTask = {
createdAt: Date.now(),
task,
status: "pending",
initialTime: time,
pausedTime: "",
userId: user.uid,
};
dispatch(addTask(newTask));
setTask("");
setTime(300);
};import { createSlice } from "@reduxjs/toolkit";
import { createAsyncThunk } from "@reduxjs/toolkit";
import {
addDoc,
getDocs,
deleteDoc,
doc,
collection,
updateDoc,
} from "firebase/firestore";
import { db } from "../firebase";
// ๋ชจ๋ ํ์คํฌ ๊ฐ์ ธ์ค๊ธฐ
export const getAllTasks = createAsyncThunk(
"tasks/getAllTasks",
async (userId) => {
const taskCollection = await getDocs(collection(db, "tasks"));
const allTasks = [
...taskCollection.docs.map((doc) => ({ ...doc.data(), id: doc.id })),
].filter((task) => task.userId === userId);
return allTasks;
}
);
// ํ์คํฌ ์ถ๊ฐํ๊ธฐ
export const addTask = createAsyncThunk("tasks/addTask", async (data) => {
const result = await addDoc(collection(db, "tasks"), {
userId: data?.userId,
task: data?.task,
initialTime: data?.initialTime,
pausedTime: "",
createdAt: data?.createdAt,
status: data.status,
});
const taskId = result.path.split("/")[1];
return { ...data, id: taskId };
});
// ํ์คํฌ ์ญ์ ํ๊ธฐ
export const deleteTask = createAsyncThunk(
"tasks/deleteTask",
async (taskId) => {
console.log(taskId);
await deleteDoc(doc(db, "tasks", taskId));
return taskId;
}
);
// ๋ฉ์ถ ํ์คํฌ ์ถ๊ฐํ๊ธฐ
export const putTask = createAsyncThunk("tasks/putTask", async (task) => {
if (task.pausedTime !== "") {
await updateDoc(doc(db, "tasks", task.id), {
pausedTime: task?.pausedTime,
status: task.status,
});
}
return task;
});
// ์๋ฃ๋ ํ์คํฌ ์ถ๊ฐํ๊ธฐ
export const completedTask = createAsyncThunk(
"tasks/completedTask",
async (task) => {
if (task?.status === "complete") {
await updateDoc(doc(db, "tasks", task.id), {
status: task?.status,
});
}
return task;
}
);
// ๋ชจ๋ 'complete task' ์ญ์ ํ๊ธฐ
export const allClear = createAsyncThunk(
"tasks/allClearTask",
async (userId) => {
const taskCollection = await getDocs(collection(db, "tasks"));
const tasksToDelete = taskCollection.docs.filter(
(doc) => doc.data().userId === userId && doc.data().status === "complete"
);
const deletePromises = tasksToDelete.map((task) =>
deleteDoc(doc(db, "tasks", task.id))
);
await Promise.all(deletePromises);
return tasksToDelete.map((task) => task.id);
}
);
const initialState = {
tasks: [],
loading: false,
error: null,
};
const tasksSlice = createSlice({
name: "tasks",
initialState,
extraReducers: (builder) => {
builder.addCase(getAllTasks.pending, (state) => {
state.loading = true;
});
builder.addCase(getAllTasks.fulfilled, (state, action) => {
state.loading = false;
state.tasks = action.payload;
});
builder.addCase(getAllTasks.rejected, (state) => {
state.loading = false;
});
builder.addCase(addTask.pending, (state) => {
state.loading = true;
});
builder.addCase(addTask.fulfilled, (state, action) => {
state.loading = false;
state.tasks.push(action.payload);
});
builder.addCase(addTask.rejected, (state) => {
state.loading = false;
});
builder.addCase(deleteTask.pending, (state) => {
state.loading = true;
});
builder.addCase(deleteTask.fulfilled, (state, action) => {
state.loading = false;
state.tasks = state.tasks.filter((task) => task.id !== action.payload);
});
builder.addCase(deleteTask.rejected, (state) => {
state.loading = false;
});
builder.addCase(putTask.pending, (state) => {
state.loading = true;
});
builder.addCase(putTask.fulfilled, (state, action) => {
state.loading = false;
state.tasks = state.tasks.map((task) =>
task.id === action.payload.id ? action.payload : task
);
});
builder.addCase(putTask.rejected, (state) => {
state.loading = false;
});
builder.addCase(completedTask.pending, (state) => {
state.loading = true;
});
builder.addCase(completedTask.fulfilled, (state, action) => {
state.loading = false;
state.tasks = state.tasks.filter((task) => task.id !== action.payload.id);
window.alert("Task saved successfully ๐");
});
builder.addCase(completedTask.rejected, (state) => {
state.loading = false;
});
builder.addCase(allClear.pending, (state) => {
state.loading = true;
});
builder.addCase(allClear.fulfilled, (state, action) => {
state.loading = false;
state.tasks = state.tasks.filter((task) => !action.payload.some(task.id));
});
builder.addCase(allClear.rejected, (state) => {
state.loading = false;
});
},
});
export default tasksSlice.reducer;
๋์๊ฒ ํ์ํ ๋ฐ์ดํฐ์ ํ๋กํผํฐ๋ฅผ ๋ช ํํ ํจ์ผ๋ก์จ, db์ ๋ถ์ฐ์ ์ค์ฌ, ํจ์จ์ ์ธ ์ํ ๊ด๋ฆฌ๋ฅผ ์ํํ๊ฒ ๋ค๋ ๊ฒ์ด ํต์ฌ์ด๋ค.
โ ์์ธ์ฒ๋ฆฌ
1. ํ์ ๊ฐ์ ์ ํ์ํ ์์ธ์ฒ๋ฆฌ๋ฅผ util๋ก ๋ถ๋ฆฌ โ๏ธ
export const validateUsername = (username) => {
const usernameRegex = /^[a-zA-Z0-9_]{3,16}$/;
return usernameRegex.test(username)
? ""
: "3์ ์ด์ 16์ ์ดํ, ์๋ฌธ์, ์ซ์, ๋ฐ์ค(_)๋ง ํ์ฉ๋ฉ๋๋ค.";
};
export const validateEmail = (email) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email) ? "" : "์๋ชป๋ ์ด๋ฉ์ผ ํ์์
๋๋ค.";
};
export const validatePassword = (password) => {
const passwordRegex =
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
return passwordRegex.test(password)
? ""
: "์ต์ 8์ ์ด์, ๋๋ฌธ์, ์๋ฌธ์, ์ซ์, ํน์๋ฌธ์๋ฅผ ํฌํจํด์ผ ํฉ๋๋ค.";
};
2. ๋ก๊ทธ์ธ ์ฌ๋ถ์ ๋ฐ๋ฅธ ๋ผ์ฐํ ์์ธ ์ฒ๋ฆฌ โ๏ธ
import { useEffect } from "react";
import { useNavigate, useLocation } from "react-router-dom";
const usePreventAuth = () => {
const navigate = useNavigate();
const { pathname } = useLocation();
useEffect(() => {
if (JSON.parse(localStorage.getItem("user"))) {
navigate("/home");
if (pathname === "/archive") {
navigate("/archive");
}
} else {
if (pathname === "/home" || pathname === "/archive") {
navigate("/");
}
}
}, []);
return;
};
export default usePreventAuth;3. loading์ ํตํ, (API ํต์ ์ ) Button disabled ์ฒ๋ฆฌ โ๏ธ
<Button disabled={loading} onClick={handleSignIn}>
Sign In
</Button>const Button = styled.button`
display: flex;
align-items: center;
justify-content: center;
width: 300px;
padding: 1rem 0;
margin: 1rem;
font-size: 1.5rem;
color: black;
background: white;
border: 2px solid white;
border-radius: 50px;
box-shadow: 0px 4px 15px rgba(255, 255, 255, 0.2);
transition: all 0.3s ease-in-out;
cursor: pointer;
opacity: 0;
animation: fadeIn 1.5s ease forwards;
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
}
}
&:hover {
background: #ffffff;
color: black;
transform: translateY(-5px);
box-shadow: 0px 8px 20px rgba(255, 255, 255, 0.3);
}
&:active {
transform: translateY(0);
box-shadow: 0px 4px 15px rgba(255, 255, 255, 0.2);
}
&:disabled {
background-color: gray;
}
`;4. .env๋ฅผ ํตํ ํ๊ฒฝ๋ณ์ ์ฒ๋ฆฌ โ๏ธ
import { initializeApp } from "firebase/app";
import { getAuth, onAuthStateChanged } from "firebase/auth";
import { getFirestore } from "firebase/firestore";
import { getStorage } from "firebase/storage";
// Your web app's Firebase configuration
const firebaseConfig = {
apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
authDomain: "task-tracker3.firebaseapp.com",
projectId: "task-tracker3",
storageBucket: "task-tracker3.appspot.com",
messagingSenderId: "977756658453",
appId: "1:977756658453:web:588402c61ac23db27c671c",
};
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const db = getFirestore(app);
const storage = getStorage(app); // Initialize storage
export { auth, db, storage, onAuthStateChanged };โ ํ๊ณ
์ค๋ฌด ์ด์ด ๊ฐ ๋์์ ๋ ์ ๊น ํ๋ฆฌ๋ฐ๊ฒ๋จ์์ ์๋ฅด๋ฐ์ดํธ๋ฅผ ํ ์ ์ด ์๋ค. ํ ํ ๋จธ๋๊ฐ ๋ฐ๋ฏํ ๋ฌผ์ ์ค ์ ์๋๊ณ ์ฌ์ญค๋ณด์ ์ ๋จ๊ฑฐ์ด ๋ฌผ์ ์ฐฌ๋ฌผ์ ์ข ํ๊ณ ์์๋๋ฐ, ์ค์ฅ๋์ด ์ฐฌ๋ฌผ์ ์ ๋ถ๋๊ณ ํธํต ์๋ ํธํต์ ์ณค๋ค. ๊ทธ๋ฐ๋ฐ ํ ๋จธ๋์ ์์ผ์ จ๋ค. '๋จ๊ฑฐ์ด' ๋ฌผ์ด ์๋๋ผ, '๋ฐ๋ฏํ' ๋ฌผ์ ๋ฌ๋ผ๊ณ ํ์ผ๋ ๋ด๊ฐ ๋ง๋ค๊ณ ๋ง์ํ์ จ๋ค. ๋ฌผ๋ก ์ค์ฅ๋๋ ์ฌ๊ณผํ์ จ๋ค.
๊ฐ๋์ ๋ํ ์ผ์์ ์จ๋ค. ์ฌ๋ฐ๋ ์ ์, ๊ทธ ๋ํ ์ผ์ด๋ผ๋ ๊ฒ์ ๋๋จํ ํ๋จ์ด ์๋๋ผ ์ธ์ฌํ ๊ด์ฐฐ๋ก๋ถํฐ ์ป์ ์ ์๋ค. ๋จธ๋ฆฌํธ ๋น ์ง ๋๊น์ง ์ณ๋ค๋ณธ ํ๋ก๋ํธ๋ ํ๋ฆฌํฐ๊ฐ ์ข์ ์๋ฐ์ ์๊ณ , ์๋ง๋ ์ฌ์ฉ์์๊ฒ ๊ฐ๋์ผ๋ก ๋ค๊ฐ๊ฐ ๊ฒ์ด๋ค. ๋ฌผ๋ก ๊ทธ๋ ์ง ์๋๋ผ๋ ๋น์ฐํ ๋ง์ด ๊ด์ฐฐํด์ผ ๋๋ค. ๊ธฐ๋ณธ์ด๋๊น. '๋ถ์ง๋ฐํจ'์ ๋ํ ๊ฒฝ๊ฐ์ฌ์ด ์ด๋ฒ ํ๋ก์ ํธ์ ์ต๋ ๊ตํ ์๋์์๊น.
More to read
ํ๋ก ํธ์๋์ ๋ฐฑ์๋ ์ฌ์ด
HTTP ์ํ ์ฝ๋๋ ํ๋ก ํธ์๋์์ ๋ฐฑ์๋๋ก ๋ณด๋๋ ์์ฒญ์ ์ํ ๊ฒฐ๊ณผ๋ฅผ ์๋ฏธํ๋ ์ผ์ข ์ ์ฝ์์ด๋ฉฐ, API๋ฅผ ๊ตฌ์ฑํ๋ ํต์ฌ ์์ ์ค ํ๋์ ๋๋ค. ์ํ ์ฝ๋์ ๊ด๋ จํ์ฌ, ๋ฐฑ์๋๋ ์ ๋ชจ๋ฅด๋ ํ๋ก ํธ์๋์ ์ฌํ ์ฌ์ ์ด ์์ต๋๋ค.์๋๋ ์์ฒญ์ด ์คํจํ์์๋, ๋ฐฑ์๋์์ ์ํ ์ฝ๋
JWTํ ํฐ ๊ด๋ฆฌ ๋ฐฉ์ ํบ์๋ณด๊ธฐ
0. ๋ค์ด๊ฐ๋ฉฐ ๐ฏ ์๋น์ค์ ์ ๊ทผํ๋ ค๋ ์ฌ์ฉ์๊ฐ ๋๊ตฌ์ธ์ง ํ์ธํ๋ ๊ณผ์ ์ ์ฌ์ฉ์ ์ธ์ฆ์ด๋ผ๊ณ ํฉ๋๋ค. ์ธ์ฆ๋ ์ฌ์ฉ์์๊ฒ ์ฃผ์ด์ง ๊ถํ์ ํ์ธํ๋ ์์ ์ ์ธ๊ฐ๋ผ๊ณ ๋ถ๋ฆ ๋๋ค. ์ด๋ฒ ๊ธ์์๋ ์ธ๊ฐ๋ ๋ค๋ฃจ์ง ์์ต๋๋ค. ์ฌ์ฉ์ ์ธ์ฆ์๋ ๋ง์ ๋ฐฉ์์ด ์์ง๋ง, ์ค๋์ ์ธ์ ์ธ์ฆ ๋ฐฉ
A2AA2A / MCP ๋ฉํฐ ์์ด์ ํธ ์ค์ผ์คํธ๋ ์ด์
0. ๋ค์ด๊ฐ๋ฉฐ โ๏ธ Google for Developers์, ๋ ์คํ ๋ ๊ณต๊ธ๋ง ์๋๋ฆฌ์ค๋ก ์ฎ์ 6๋ ํ๋กํ ์ฝ(MCP, A2A, UCP, AP2, A2UI, AG-UI)์ ๋ํ ๊ฐ์ด๋๊ฐ ๊ฒ์๋ ์ดํ, MCP์ A2A๋ถํฐ ๊ตฌํํด ๋ณด๋ ๊ฒ์ด ์ข์ ๊ฒ ๊ฐ๋ค๋ ์๊ฐ์ด ๋ค์์ต๋