React 리액트/React 리액트

React 비지니스로직, UI로직 분리하기, 효율적인 폴더구조 짜기

솧디_code 2022. 12. 5. 02:04

  1. common 폴더를 components 폴더로 폴더명 변경
    1. 그에 따른 경로 설정 변경
      1. 만약 경로가 너무 복잡하게 엉킬 경우, common으로 되돌리기!
  2. componsnts 폴더를 container 폴더로 폴더명 변경
    1. 그에 따른 경로 설정 변경
  3. container 폴더 하위에 있는 기능들을 UI로직과 비즈니스 로직으로 나눠 구분
    1. UI로직은 각각 UI에 보여지는 이름으로 폴더명 작명
      1. index.js
      2. UI명을 딴(폴더명과 동일한) 컴포넌트(확장자 jsx)
      3. styles.js

⭐️참고 구조⭐️

  1. 폰트 사이즈 전체적으로 축소하기
    1. theme.js 에서 폰트 사이즈 수정
  2. 모바일 style props명 작명
    1. 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 , 폴더구조 및

페이지 하나의 기능들을 분리하고 어떠한 기능들이 있는지 폴더구조만 봐도 알수있게 수정하였다.