본문 바로가기

Project Memoir/Hanium (2021)

[Project] React | 재사용을 위한 폴더 정리

사실 이번 한이음 프로젝트에서는 프론트와 백을 동시에 4명이서 배워보자는 취지였기에,

폴더 구성 및 컴포넌트 설계 방식을 정하고 개발을 시작하기 보다는 그냥 구현할 곳을 분배하고 구현하기에 바빴던 것 같다.

 

어느정도 웹사이트 구현을 마치고, 오늘(2021/09/25) 멘토님과의 미팅에서 이제껏 작성한 코드와 폴더를 살펴보다가 멘토님이 이부분에 대해 이야기를 시작하셨다.

 

 

1️⃣ 폴더 구조

실제 현업에서의 리액트 폴더 구조는 다음과 같이 구성한다고 한다.

 

📁components : 컴포넌트 파일들이 위치하는 폴더

📁adapter : xhr(axios) + socket + websocket 등

📁store : redux 작업을 위한 폴더, 내부에 actions, reducers 폴더 존재

📁hooks : "use~"라고 쓴 컴포넌트

📁utils 

📁domain (해당 도메인 (프로젝트)에서 쓰이는 ,,)

    📁components

    📁adapter

    📁store

    📁hooks

    📁utils 

 

-> 파란색 : 재사용 가능

 

 

 

2️⃣ logic, view의 분리

또한 components안에 들어있을 페이지를 만드는 logic과 view를 한 컴포넌트에 넣지 않고 둘을 두개의 파일로 분리하여 작업하는 것에 대한 설명도 더하셨다.

  • logic = container
  • view = presentation

 

페이지 하나를 구성하는 파일 하나를 logic과 view로 분리해보자

일단, 파일 하나안에 logic과 view가 모여있는 코드이다.

import React, { useState, useEffect } from "react";
import "react-datepicker/dist/react-datepicker.css";
import { Button, Form, FormGroup, Label, Input, Col } from "reactstrap";
import { useHistory, useParams } from "react-router-dom";
import { useAssignments, useTeams } from "../../../components/Use";
import styled from "styled-components";

const Box = styled.div`
  width: 80%;
  button {
    font-family: Roboto;
    font-style: normal;
    font-weight: 400;
    font-size: 17px;
    text-align: center;
    background-color: #ffffff;
    border-color: #426589;
    color: #426589;
    width: 75px;
    height: 30px;
  }
`;

const P09_07 = () => {
  const history = useHistory();
  const { code } = useParams();

  const { createAssignmentsApi } = useAssignments();
  const { teamList, listAllTeams } = useTeams();

  const [data, setData] = useState({
    name: "",
    content: "",
  });

  const [teams, setTeams] = useState([]);

  const checkboxChange = (e) => {
    setTeams({
      ...teams,
      [e.target.name]: e.target.checked,
    });
  };

  const [image, setImage] = useState(null);

  const createHandler = async () => {
    const team = [];
    const keys = Object.keys(teams);
    for (let i = 0; i < keys.length; i++) {
      const key = keys[i];
      const value = teams[key];
      if (value === true) team.push(key);
    }

    const formData = new FormData();
    formData.append("name", data.name);
    formData.append("point", data.point);
    formData.append("startDate", data.startDate);
    formData.append("endDate", data.endDate);
    formData.append("content", data.content);
    formData.append("progress", 1);
    formData.append("classCode", code);
    formData.append("teams", team);
    formData.append("image", image);
    console.log(image);
    try {
      await createAssignmentsApi(formData);
      history.push(`/professor/class/${code}/assignmentList`);
    } catch (e) {
      alert(e);
    }
  };

  useEffect(() => {
    const fetch = async () => {
      try {
        await listAllTeams("AZSVBFV");
      } catch (e) {
        alert(e);
      }
    };
    fetch();
  }, []);

  const imageChange = (e) => {
    setImage(e.target.files[0]);
  };

  const handleChange = (e) => {
    setData({
      ...data,
      [e.target.name]: e.target.value,
    });
  };

  console.log(teams);
  console.log(JSON.stringify(teams));

  return (
    <Box>
      <Form>
        <button size="sm" style={{ marginTop: "20px" }} onClick={createHandler}>
          완료
        </button>
        <FormGroup
          row
          style={{
            marginLeft: 3,
            padding: "15px 0px",
            borderBottom: "1px solid #C4C4C4",
          }}
        >
          <Label
            for="name"
            sm={1}
            style={{ fontWeight: "bold", paddingLeft: 0 }}
          >
            과제명
          </Label>
          <Col sm={10}>
            <Input
              type="name"
              name="name"
              id="name"
              value={data.name}
              onChange={handleChange}
            />
          </Col>
        </FormGroup>
        <FormGroup
          row
          style={{
            marginLeft: 3,
            padding: "15px 0px",
            borderBottom: "1px solid #C4C4C4",
          }}
        >
          <Label
            for="point"
            sm={1}
            style={{ fontWeight: "bold", paddingLeft: 0 }}
          >
            배점
          </Label>
          <Col sm={3}>
            <Input
              type="point"
              name="point"
              id="point"
              value={data.point}
              onChange={handleChange}
            />
          </Col>
        </FormGroup>
        <FormGroup
          row
          style={{
            marginLeft: 3,
            padding: "15px 0px",
            borderBottom: "1px solid #C4C4C4",
          }}
        >
          <Label
            for="point"
            sm={1}
            style={{ fontWeight: "bold", paddingLeft: 0 }}
          >
            공개일
          </Label>
          <Col sm={3}>
            <Input
              type="datetime-local"
              name="startDate"
              id="startDate"
              value={data.startDate}
              onChange={handleChange}
            />
          </Col>
        </FormGroup>
        <FormGroup
          row
          style={{
            marginLeft: 3,
            padding: "15px 0px",
            borderBottom: "1px solid #C4C4C4",
          }}
        >
          <Label
            for="point"
            sm={1}
            style={{ fontWeight: "bold", paddingLeft: 0 }}
          >
            마감일
          </Label>
          <Col sm={3}>
            <Input
              type="datetime-local"
              name="endDate"
              id="endDate"
              value={data.endDate}
              onChange={handleChange}
            />
          </Col>
        </FormGroup>
        <FormGroup
          row
          style={{
            marginLeft: 3,
            padding: "15px 0px",
            borderBottom: "1px solid #C4C4C4",
            alignItems: "center",
          }}
        >
          <Label
            for="point"
            sm={1}
            style={{ fontWeight: "bold", paddingLeft: 0 }}
          >
            팀지정
          </Label>
          {teamList.results.map((team) => (
            <Col sm={1}>
              <Input
                type="checkbox"
                name={team.id}
                onChange={checkboxChange}
                style={{ marginRight: "5px" }}
              />
              {team.name}
            </Col>
          ))}
        </FormGroup>
        <FormGroup style={{ marginTop: "30px" }}>
          <Input
            type="textarea"
            name="content"
            id="assignmentText"
            value={data.content}
            onChange={handleChange}
            style={{ height: "300px" }}
          />
        </FormGroup>
        <FormGroup style={{ marginTop: "20px" }}>
          <Label
            for="imageFile"
            sm={1}
            style={{ fontWeight: "bold", paddingLeft: 0 }}
          >
            첨부 파일
          </Label>
          <Input type="file" onChange={imageChange} />
        </FormGroup>
        <FormGroup style={{ marginTop: "10px" }}>
          <Label for="solutionFile" sm={1} style={{ fontWeight: "bold" }}>
            해답 파일
          </Label>
          <Input type="file" name="solutionFile" id="solutionFile" />
        </FormGroup>
      </Form>
    </Box>
  );
};

export default P09_07;

보다시피 logic과 view가 한 파일에 있기에 매우 길고 눈에 확 들어오지 않는 코드이다.

또한 재사용될 수도 있는 logic을 분리해 놓지 않았다는 비효율성도 보인다.

 

이제, 나누어보자

첫번째로 logic 부분의 파일이다.

import React, { useState, useEffect } from "react";
import "react-datepicker/dist/react-datepicker.css";
import { Button, Form, FormGroup, Label, Input, Col } from "reactstrap";
import { useHistory, useParams } from "react-router-dom";
import { useAssignments, useTeams } from "../../../components/Use";
import styled from "styled-components";


const P09_07 = () => {
  const history = useHistory();
  const { code } = useParams();

  const { createAssignmentsApi } = useAssignments();
  const { teamList, listAllTeams } = useTeams();

  const [data, setData] = useState({
    name: "",
    content: "",
  });

  const [teams, setTeams] = useState([]);

  const checkboxChange = (e) => {
    setTeams({
      ...teams,
      [e.target.name]: e.target.checked,
    });
  };

  const [image, setImage] = useState(null);

  const createHandler = async () => {
    const team = [];
    const keys = Object.keys(teams);
    for (let i = 0; i < keys.length; i++) {
      const key = keys[i];
      const value = teams[key];
      if (value === true) team.push(key);
    }

    const formData = new FormData();
    formData.append("name", data.name);
    formData.append("point", data.point);
    formData.append("startDate", data.startDate);
    formData.append("endDate", data.endDate);
    formData.append("content", data.content);
    formData.append("progress", 1);
    formData.append("classCode", code);
    formData.append("teams", team);
    formData.append("image", image);
    console.log(image);
    try {
      await createAssignmentsApi(formData);
      history.push(`/professor/class/${code}/assignmentList`);
    } catch (e) {
      alert(e);
    }
  };

  useEffect(() => {
    const fetch = async () => {
      try {
        await listAllTeams("AZSVBFV");
      } catch (e) {
        alert(e);
      }
    };
    fetch();
  }, []);

  const imageChange = (e) => {
    setImage(e.target.files[0]);
  };

  const handleChange = (e) => {
    setData({
      ...data,
      [e.target.name]: e.target.value,
    });
  };

  console.log(teams);
  console.log(JSON.stringify(teams));

  return{
    imageChange, handleChange, checkboxChange, teamList, createHandler, data, teams, image
  }
};

export default P09_07;

return에 view에 보낼 기능들을 모두 적는다.

 

이제, view 부분의 파일이다.

import React, { useState, useEffect } from "react";
import "react-datepicker/dist/react-datepicker.css";
import { Button, Form, FormGroup, Label, Input, Col } from "reactstrap";
import { useHistory, useParams } from "react-router-dom";
import { useAssignments, useTeams } from "../../../components/Use";
import styled from "styled-components";
import useAddAssignment from "./useAddAssignment";


const Box = styled.div`
  width: 80%;
  button {
    font-family: Roboto;
    font-style: normal;
    font-weight: 400;
    font-size: 17px;
    text-align: center;
    background-color: #ffffff;
    border-color: #426589;
    color: #426589;
    width: 75px;
    height: 30px;
  }
`;

const AssignmentView = () => {
    const {imageChange, handleChange, checkboxChange, teamList, createHandler, data, teams, image} = useAddAssignment();
        return (
            <Box>
              <Form>
                <button size="sm" style={{ marginTop: "20px" }} onClick={createHandler}>
                  완료
                </button>
                <FormGroup
                  row
                  style={{
                    marginLeft: 3,
                    padding: "15px 0px",
                    borderBottom: "1px solid #C4C4C4",
                  }}
                >
                  <Label
                    for="name"
                    sm={1}
                    style={{ fontWeight: "bold", paddingLeft: 0 }}
                  >
                    과제명
                  </Label>
                  <Col sm={10}>
                    <Input
                      type="name"
                      name="name"
                      id="name"
                      value={data.name}
                      onChange={handleChange}
                    />
                  </Col>
                </FormGroup>
                <FormGroup
                  row
                  style={{
                    marginLeft: 3,
                    padding: "15px 0px",
                    borderBottom: "1px solid #C4C4C4",
                  }}
                >
                  <Label
                    for="point"
                    sm={1}
                    style={{ fontWeight: "bold", paddingLeft: 0 }}
                  >
                    배점
                  </Label>
                  <Col sm={3}>
                    <Input
                      type="point"
                      name="point"
                      id="point"
                      value={data.point}
                      onChange={handleChange}
                    />
                  </Col>
                </FormGroup>
                <FormGroup
                  row
                  style={{
                    marginLeft: 3,
                    padding: "15px 0px",
                    borderBottom: "1px solid #C4C4C4",
                  }}
                >
                  <Label
                    for="point"
                    sm={1}
                    style={{ fontWeight: "bold", paddingLeft: 0 }}
                  >
                    공개일
                  </Label>
                  <Col sm={3}>
                    <Input
                      type="datetime-local"
                      name="startDate"
                      id="startDate"
                      value={data.startDate}
                      onChange={handleChange}
                    />
                  </Col>
                </FormGroup>
                <FormGroup
                  row
                  style={{
                    marginLeft: 3,
                    padding: "15px 0px",
                    borderBottom: "1px solid #C4C4C4",
                  }}
                >
                  <Label
                    for="point"
                    sm={1}
                    style={{ fontWeight: "bold", paddingLeft: 0 }}
                  >
                    마감일
                  </Label>
                  <Col sm={3}>
                    <Input
                      type="datetime-local"
                      name="endDate"
                      id="endDate"
                      value={data.endDate}
                      onChange={handleChange}
                    />
                  </Col>
                </FormGroup>
                <FormGroup
                  row
                  style={{
                    marginLeft: 3,
                    padding: "15px 0px",
                    borderBottom: "1px solid #C4C4C4",
                    alignItems: "center",
                  }}
                >
                  <Label
                    for="point"
                    sm={1}
                    style={{ fontWeight: "bold", paddingLeft: 0 }}
                  >
                    팀지정
                  </Label>
                  {teamList.results.map((team) => (
                    <Col sm={1}>
                      <Input
                        type="checkbox"
                        name={team.id}
                        onChange={checkboxChange}
                        style={{ marginRight: "5px" }}
                      />
                      {team.name}
                    </Col>
                  ))}
                </FormGroup>
                <FormGroup style={{ marginTop: "30px" }}>
                  <Input
                    type="textarea"
                    name="content"
                    id="assignmentText"
                    value={data.content}
                    onChange={handleChange}
                    style={{ height: "300px" }}
                  />
                </FormGroup>
                <FormGroup style={{ marginTop: "20px" }}>
                  <Label
                    for="imageFile"
                    sm={1}
                    style={{ fontWeight: "bold", paddingLeft: 0 }}
                  >
                    첨부 파일
                  </Label>
                  <Input type="file" onChange={imageChange} />
                </FormGroup>
                <FormGroup style={{ marginTop: "10px" }}>
                  <Label for="solutionFile" sm={1} style={{ fontWeight: "bold" }}>
                    해답 파일
                  </Label>
                  <Input type="file" name="solutionFile" id="solutionFile" />
                </FormGroup>
              </Form>
            </Box>
          );
}

export default AssignmentView

logic을 불러와서 화면상에 뿌려주는 코드만이 적혀져있다.

 

❗️효과❗️

  • view부분의 코드가 화면에 뿌려주는게 많은 만큼 길지만, 그래도 코드도 많이 줄었다.
  • logic과 view를 분리해서 볼 수 있다.
  • logic을 다른 view에서 재사용하기 쉽다.

 


이미 웹 사이트 구현은 백, 프론트 모두 어느정도 진행되어버린 프로젝트이고,

마무리되면 바로 다음달 부터는 텍스트 마이닝 등 쌓여진 데이터를 활용하는 법도 배워야하기 때문에

멘토님이 말씀해 주신 폴더 구조나 컴포넌트 분리방식은 따르기에 너무 늦고 힘들다고 판단했다..!

 

그래도 폴더 구성을 어떻게 짤지와 component 분리를 어떤 식으로 할지 미리 정하면 매우 효율적이고 재사용성도 높아진다는 것을 깨달았다.

다른 프로젝트를 하게된다면 처음에 이를 잡고 시작해야겠다..ㅠ 

'Project Memoir > Hanium (2021)' 카테고리의 다른 글

2차 심사 결과,,  (0) 2021.11.02
[PROJECT] React | Select Box의 Option 값 전달하기  (0) 2021.09.13