React 리액트/React 리액트
React 비지니스로직, UI로직 분리하기, 효율적인 폴더구조 짜기
솧디_code
2022. 12. 5. 02:04
- common 폴더를 components 폴더로 폴더명 변경
- 그에 따른 경로 설정 변경
- 만약 경로가 너무 복잡하게 엉킬 경우, common으로 되돌리기!
- 그에 따른 경로 설정 변경
- componsnts 폴더를 container 폴더로 폴더명 변경
- 그에 따른 경로 설정 변경
- container 폴더 하위에 있는 기능들을 UI로직과 비즈니스 로직으로 나눠 구분
- UI로직은 각각 UI에 보여지는 이름으로 폴더명 작명
- index.js
- UI명을 딴(폴더명과 동일한) 컴포넌트(확장자 jsx)
- styles.js
- UI로직은 각각 UI에 보여지는 이름으로 폴더명 작명
⭐️참고 구조⭐️
- 폰트 사이즈 전체적으로 축소하기
- theme.js 에서 폰트 사이즈 수정
- 모바일 style props명 작명
- xl, lg, md, sm, xs 과 유사하게..
✍🏻 비니지스,UI 로직 분리 전
한 컴포넌트 안에 기능로직과 뷰포인트 ui로직이 함께있어 코드가 길다.
//components/cafeReview/CafeReview.jsx
import {
Box,
Input,
Button,
FirstHeading,
Image,
Label,
Margin,
DataList,
DataTerm,
DataDesc,
Hidden,
Flex,
ThirdHeading,
TextArea,
SecondHeading,
} from "../../common";
import { useState, useEffect } from "react";
import { useMutation } from "@tanstack/react-query";
import { CafeSearch, CafeRatings } from "../../components/cafeReview";
import { cafe_review_image_upload } from "../../assets/icons";
import { useNavigate } from "react-router-dom";
import axios from "axios";
import Edit from "./edit";
import { useMutation, useQueryClient, useQuery } from "@tanstack/react-query";
import axios from "axios";
import { useNavigate, useParams } from "react-router-dom";
import { useState } from "react";
import Spinner from "../../assets/icons/spinner.gif";
const ComuEdit = () => {
const BASE_URL = process.env.REACT_APP_SERVER;
const { id } = useParams();
const navigate = useNavigate();
//이미지 수정 여부 스테이트
const [editImgSrc, setEditImgSrc] = useState(false);
// 이미지 미리보기 스테이트
const [imageSrc, setImageSrc] = useState("");
//로컬스토리지 토큰가져오기
const authorization = localStorage.getItem("Authorization");
//queryClient 선언하기
const queryClient = useQueryClient();
//커뮤니티 수정 게시물 목록 get요청
const { data, isError, isLoading, refetch } = useQuery({
queryKey: ["community"],
queryFn: async () => {
try {
const response = await axios.get(`${BASE_URL}/community/${id}`);
console.log("response =====>", response.data);
return response.data;
} catch (error) {
console.log("error =>", error);
return error;
}
},
suspense: true,
});
//수정 내용 저장 스테이트
const init = {
communityTitle: data.communityTitle,
communityContent: data.communityContent,
};
const [edit, setEdit] = useState(init);
const [editImg, setEditImg] = useState("");
//텍스트데이터 스테이즈 저장
const onChangeEdit = e => {
const { name, value } = e.target;
setEdit({ ...edit, [name]: value });
};
//이미지 스테이트저장, 미리보기 온체인지 핸들러
const onChangeImage = e => {
setEditImgSrc(!editImgSrc);
const { name, files } = e.target;
setEditImg(files[0]);
let reader = new FileReader();
if (files[0]) {
reader.readAsDataURL(files[0]);
}
reader.onloadend = () => {
const previewImgUrl = reader.result;
if (previewImgUrl) {
setImageSrc([...imageSrc, previewImgUrl]);
}
};
};
// 댓글 수정하기 put요청
const { mutate: editMutation } = useMutation(
async comuEdit => {
const response = await axios.put(
`${BASE_URL}/auth/update/community/${id}`,
comuEdit,
{
headers: {
authorization,
},
},
);
return response;
},
{
onSuccess: () => {
queryClient.invalidateQueries("communityDetail");
alert("게시물이 수정되었습니다.");
navigate(`/community/${id}`);
},
onError: error => {
alert("수정되지않았어요🥹");
navigate(`/community/${id}`);
},
},
);
//게시물 수정하기 쿼리 요청(온클릭)
const onClickHandler = e => {
e.preventDefault();
const formData = new FormData();
formData.append("data", JSON.stringify(edit));
// formData.append("image", editImg);
if (editImg !== null) {
formData.append("image", editImg);
}
editMutation(formData);
let entries = formData.entries();
for (const pair of entries) {
console.log(pair[0] + ", " + pair[1]);
}
if (edit && editImg === "") {
alert("수정내용이 없습니다.");
} else {
editMutation(
{
communityTitle: formData.append("data", JSON.stringify(edit)),
// communityContent: formData.append(
// "data",
// JSON.stringify(edit.communityContent),
// ),
communityImage: formData.append("image", editImg),
},
{
onError: (error, variables, context) => {
console.log("error => ", error);
},
onSuccess: (data, variables, context) => {
queryClient.invalidateQueries("communityDetail");
alert(data.data);
},
},
);
}
setEdit(false);
};
if (isLoading)
return (
<div>
<img src={Spinner} alt={"로딩중"} />
</div>
);
if (isError) return <div>에러입니다.</div>;
return (
<>
<input
type="text"
name="communityTitle"
defaultValue={data?.communityTitle}
required={data?.communityTitle}
onChange={onChangeEdit}
/>
<input
type="text"
name="communityContent"
defaultValue={data?.communityContent}
required={data?.communityContent}
onChange={onChangeEdit}
/>
<input
name="editImg"
type={"file"}
accept={"image/*"}
placeholder="이미지업로드"
onChange={onChangeImage}
/>
{editImgSrc ? (
<>
<img src={imageSrc} alt={"수정이미지"} />
</>
) : (
<img src={data?.communityImage} alt={data?.communityTitle} />
)}
<button onClick={onClickHandler}>수정완료</button>
</>
);
};
export default ComuEdit;
✍🏻 수정 전 components , 폴더구조
컴포넌트 안의 폴더구조만 보더라도 어떤기능인지 명확하게하기위해 개선
✍🏻 비니지스,UI 로직 분리 후
import { useNavigate, useParams } from "react-router-dom";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import axios from "axios";
import Detail from "./detail";
import ComuEdit from "./ComuEdit";
import { Box, Image } from "../../components";
import Spinner from "../../assets/icons/spinner.gif";
import { useEffect } from "react";
const ComuDetail = () => {
const { id } = useParams();
const BASE_URL = process.env.REACT_APP_SERVER;
//로컬스토리지 닉네임가져오기
const nickname = localStorage.getItem("Nickname");
const navigate = useNavigate();
//로컬스토리지 토큰가져오기
const authorization = localStorage.getItem("Authorization");
//커뮤니티 상세페이지 get요청
const { data, isError, isLoading, refetch } = useQuery({
queryKey: ["communityDetail"],
queryFn: async () => {
try {
const response = await axios.get(`${BASE_URL}/community/${id}`);
console.log("response =====>", response.data);
return response.data;
} catch (error) {
console.log("error =>", error);
return error;
}
},
suspense: true,
});
console.log("communityDetail=>", data);
console.log("isError =>", isError, "isLoading =>", isLoading);
//queryClient 선언하기
const queryClient = useQueryClient();
//댓글 삭제하기 delete요청
const delMutation = useMutation(
data => {
return axios.delete(`${BASE_URL}/auth/community/delete/${id}`, {
headers: {
authorization,
},
});
},
{
onSuccess: () => {
queryClient.invalidateQueries("community");
alert("삭제되었습니다.");
navigate("/community");
},
},
);
//댓글 삭제하기 쿼리요청
const handleRemove = e => {
e.preventDefault();
const delRes = window.confirm("정말 삭제하시겠습니까?");
if (delRes) {
delMutation.mutate({ data: data.communityId });
} else {
alert("취소합니다.");
}
};
if (isLoading)
return (
<Box>
<Image src={Spinner} alt={"로딩중"} />
</Box>
);
if (isError) return <Box>에러입니다.</Box>;
return (
<>
<Detail
data={data}
nickname={nickname}
authorization={authorization}
navigate={navigate}
onhandleRemove={handleRemove}
id={id}
/>
</>
);
};
export default ComuDetail;
⬆️ 비지니스 로직
import {
Box,
Input,
Image,
Text,
Label,
Margin,
Flex,
} from "../../../components";
import { Outlet } from "react-router-dom";
import Edit from "../../../assets/icons/edit-profile.png";
import Comment from "../../../assets/icons/comment.png";
import Heart from "../../../assets/icons/heart.png";
import Write from "../../../assets/icons/write.png";
const MypgHome = ({
onEditPost,
onDeletePost,
onChangeProfileImage,
recentlyMyBoardList,
recentlyMyCommentList,
recentlyMyHeartBoardList,
memberBoardCount,
memberCommentCount,
memberHeartCount,
memberProfileImage,
navigate,
myLikeMatch,
myBoardMatch,
myCommentMatch,
myAllMatch,
nickname,
}) => {
return (
<Box variant="container-2">
<Flex>
<Box variant="profile">
<Flex gap="0.1vw" fd="column" ai="center">
<Margin margin="10% 0 0 70%">
<Box>
<Label htmlFor="imageChange" variant="profile">
<Image variant="profile-edit" src={Edit} />
<span>프로필이미지 편집</span>
</Label>
<Input
id="imageChange"
variant="profile-edit"
type="file"
accept="image/*"
onChange={onChangeProfileImage}
/>
</Box>
</Margin>
<Image
src={memberProfileImage}
alt={"프로필 이미지"}
variant="mypage-profile"
/>
<Box variant="pofile-namebox">
<Flex jc="center" gap="5%">
<Margin margin="8%">
<Flex jc="center">
<Text variant="join">{nickname}</Text>
</Flex>
<Margin margin="10%">
<Box>
<Flex gap="5%" ai="center" jc="center">
<Text variant="level">Lv</Text>
<Text variant="level-name">톨 💛</Text>
</Flex>
</Box>
</Margin>
</Margin>
</Flex>
</Box>
<Margin margin="10% 0 0 0">
<Box variant="category-box">
<Flex gap="10%" jc="center">
<Image variant="mypage-icon" src={Write} />
<Image variant="mypage-icon" src={Heart} />
<Image variant="mypage-icon" src={Comment} />
</Flex>
</Box>
<Margin margin="2% 2% 0 0">
<Box variant="category-title-box">
<Flex jc="center" gap="7%">
<Text
variant="profile-base"
onClick={() => {
navigate("myboard");
}}
>
내가쓴글
</Text>
<Text
variant="profile-base"
onClick={() => {
navigate("mylike");
}}
>
좋아요
</Text>
<Text
variant="profile-base"
onClick={() => {
navigate("mycomment");
}}
>
작성댓글
</Text>
</Flex>
</Box>
</Margin>
</Margin>
<Margin margin="6% 0 0 0">
<Box variant="category-title-box">
<Flex gap="18%" jc="center">
<Text variant="join">{memberBoardCount}</Text>
<Text variant="join">{memberHeartCount}</Text>
<Text variant="join">{memberCommentCount}</Text>
</Flex>
</Box>
</Margin>
</Flex>
</Box>
<Margin margin="2% 0 0 39px">
<Box variant="mypage-nav">
<Flex gap="4%">
<Text
variant="button"
onClick={() => {
navigate("myall");
}}
isActive={myAllMatch !== null}
>
모두보기
</Text>
<Box>
<Flex jc="center" gap="0.2vw">
<Text
variant="button"
onClick={() => {
navigate("myboard");
}}
isActive={myBoardMatch !== null}
>
내가쓴글
</Text>
<Box variant="guide-point" isActive={myBoardMatch !== null}>
<Margin margin="0.1vw 0 0 0">
<Text
variant="button-count"
isActive={myBoardMatch !== null}
>
{memberBoardCount}
</Text>
</Margin>
</Box>
</Flex>
</Box>
<Box>
<Flex jc="center" gap="0.2vw">
<Text
variant="button"
onClick={() => {
navigate("mylike");
}}
isActive={myLikeMatch !== null}
>
좋아요한글
</Text>
<Box variant="guide-point" isActive={myLikeMatch !== null}>
<Margin margin="0.1vw 0 0 0">
<Text
variant="button-count"
isActive={myLikeMatch !== null}
>
{memberHeartCount}
</Text>
</Margin>
</Box>
</Flex>
</Box>
<Box>
<Flex jc="center" gap="0.2vw">
<Text
variant="button"
onClick={() => {
navigate("mycomment");
}}
isActive={myCommentMatch !== null}
>
작성댓글
</Text>
<Box variant="guide-point" isActive={myCommentMatch !== null}>
<Margin margin="0.1vw0 0 0">
<Text
variant="button-count"
isActive={myCommentMatch !== null}
>
{memberCommentCount}
</Text>
</Margin>
</Box>
</Flex>
</Box>
</Flex>
</Box>
{/* <Box>
<Box variant="guide">
<Text>내가 쓴 글</Text>
<Text>더보기</Text>
</Box>
<Box variant="guide">
{recentlyMyBoardList?.map(item => {
return (
<Box key={item.boardId}>
<Box>
<Image
variant="mypage-post"
src={item.imageList[0].imageUrl}
alt={item.boardTitle}
></Image>
<Text>{item.boardTitle}</Text>
</Box>
<Button onClick={handleEditPost(item)}>수정</Button>
<Button onClick={handelDeletePost(item)}>삭제</Button>
</Box>
);
})}
</Box>
</Box>
<Box>
<Text>좋아요 한 글</Text>
{recentlyMyHeartBoardList?.map(item => {
return (
<Box key={item.boardId}>
<Box key={item.boardId}>
<Image
variant="mypage-post"
src={item.imageList[0].imageUrl}
alt={item.boardTitle}
></Image>
<Text>{item.boardTitle}</Text>
</Box>
<Button onClick={handleEditPost(item)}>수정</Button>
<Button onClick={handelDeletePost(item)}>삭제</Button>
</Box>
);
})}
</Box>
<Box>
<Text>내가 작성한 댓글</Text>
{recentlyMyCommentList?.map(item => {
return (
<Box key={item.commentId}>
<Box key={item.commentId}>
<Text>{item.commentContent}</Text>
<Text>{item.boardTitle}</Text>
</Box>
<Button onClick={handleEditPost(item)}>수정</Button>
<Button onClick={handelDeletePost(item)}>삭제</Button>
</Box>
);
})}
</Box> */}
<Outlet />
</Margin>
</Flex>
</Box>
);
};
export default MypgHome;
⬆️ UI 로직
✍🏻 수정 후 components , 폴더구조 및
페이지 하나의 기능들을 분리하고 어떠한 기능들이 있는지 폴더구조만 봐도 알수있게 수정하였다.