와장창 프로젝트 경험기 #3

sham·2021년 9월 6일
0

Bobs 개발일지

목록 보기
3/6
post-thumbnail

현재 상황

MyPage.js

import React from 'react';
import Profile from 'components/Profile';
import MealStat from 'components/MealStat';
import MealLog from 'components/MealLog';
import Bann from 'components/Bann';

import './MyPage.scss';

const MyPage = () => {
  return (
    <div className="mypage">
      <></>
      <div>
        <Profile />

        <MealStat />

        <MealLog />

        <Bann />
      </div>
      <></>
    </div>
  );
};

export default MyPage;


구현한 것

  • css(혹은 Material UI) 적용해서 디자인
  • 서버의 데이터 통신을 제외한 전체적인 뼈대

남은 과제

  • 자바스크립트에서 서버로부터 데이터 주고받는 방법 익히기 (fetch, promise, async, await)
  • 받아온 데이터 파싱하기

구현

MealLog

MealLog.js

import { React, useState } from 'react';
import './MealLog.scss';
import PersonIcon from '@material-ui/icons/Person';
import RestaurantIcon from '@material-ui/icons/Restaurant';
import { makeStyles } from '@material-ui/core/styles';

const useStyles = makeStyles(() => ({
  icon: {
    color: '#15b2b3',
    fontSize: 40,
    marginTop: '5px',
  },
}));

const Group = ({ data, date }) => {
  const classes = useStyles();

  return (
    <span className="group">
      {data.map(e => (
        <ul className="group-person">
          <li>
            <PersonIcon className={classes.icon} />
          </li>
          <li className="group-person-id">{e}</li>
        </ul>
      ))}
      <text className="group-date">{date}</text>
    </span>
  );
};

const MealLog = () => {
  // 한달 동안의 식사 기록을 받아와 파싱한다.
  // 같이 식사한 사람들에 대해 배열의 요소로 저장한다.
  // 식사 횟수 만큼 쟁반 아이콘을 추가한다.
  const time = new Date();
  const classes = useStyles();

  const [log, setLog] = useState([
    [['asdf', 'qwer'], 4],
    [['zcx', 'cvb', 'bnm'], 6],
    [['ㅁㅇㄴ'], 9],
    [['ㅁㅂㅈ', 'qwewqr', 'zxc', 'asf'], 11],
    [['asd', 'asd'], 13],
    [['ㅁㅂㅈ', 'qwewqr', 'zxc', 'asf'], 15],
  ]);
  if (!time) setLog([]); // 임시 코드

  return (
    <div className="meal-log-container">
      <div className="meal-log-box">
        <text className="title">최근 식사</text>
        <div className="meal-log">
          <div className="head">
            <div className="month">{time.getMonth() + 1}</div>
            <div>
              {log.map(() => {
                return <RestaurantIcon className={classes.icon} />;
              })}
            </div>
          </div>
          <div className="body">
            {log.map(
              array => (
                <Group data={array[0]} date={array[1]} />
              ),
            )}
          </div>
        </div>
      </div>
    </div>
  );
};

export default MealLog;

이 컴포넌트부터 아이콘을 사용하게 되었을 때 Material UI icon을 사용했다.

MealLog 컴포넌트에서는 크게 요약해서 보여주는 head와 본문인 body로 나뉜다.
head 부분에는 해당 달의 정보와 해당 달 동안 먹은 식사 횟수만큼의 아이콘이 나타나고, body 부분에는 같이 식사한 사람들과 날짜가 직관적으로 구분지어져서 나타난다.

식사에 대한 정보는 log라는 하나의 배열에 저장되는데, 각 배열의 요소는 한 번의 식사를 의미한다. 요소 역시 배열로 첫번째 요소는 해당 식사에 같이 한 사람들에 대한 배열, 두번째 요소에는 식사를 한 날이 저장된다.
log.map()으로 각 요소마다 Group 컴포넌트를 만들어 리턴시킨다. props로 각각 같이 식사한 사람들과 식사한 날짜가 Group에 전달된다.


Bann

Bann.js

import { React, useState } from 'react';
import Modal from './Modal';
import BannBox from './BannBox';
import './Bann.scss';

const Bann = () => {
  const [input, setInput] = useState('');
  const [tempId, setTempId] = useState('');
  const [bannCadet, setBannCadet] = useState([]);
  const [bannModal, setBannModal] = useState(false);
  const [cancelModal, setCancelModal] = useState(false);

  const changeInput = e => {
    if (!bannModal) {
      setInput(e.currentTarget.value.replace(/[^A-Za-z]/gi, '')); // 영어만 입력되게끔
    }
  };
  const FindCadet = e => {
    e.preventDefault();
    if (!bannModal && input) {
      setBannModal(true);
      setTempId(input);
      setInput('');
    }
  };

  const closeModal = type => {
    if (type === 'Bann') {
      setBannModal(false);
    } else {
      setCancelModal(false);
    }
  };
  const cancelBann = e => {
    setCancelModal(true);
    setTempId(e.target.value);
  };

  return (
    <div className="bann-container">
      <div className="bann-box">
        <text className="title">차단 목록</text>
        <BannBox
          FindCadet={FindCadet}
          changeInput={changeInput}
          input={input}
          bannCadet={bannCadet}
          cancelBann={cancelBann}
        />
      </div>
      {bannModal && (
        <Modal
          key="modal"
          close={() => closeModal('Bann')}
          id={tempId}
          bann={setBannCadet}
          message="님을 차단하시겠습니까?"
          type="bann"
        />
      )}
      {cancelModal && (
        <Modal
          key="modal"
          close={() => closeModal('Cancel')}
          id={tempId}
          bann={setBannCadet}
          message="님을 차단해제하시겠습니까?"
          type="cancel"
        />
      )}
    </div>
  );
};

export default Bann;

아이디를 입력하고 차단 목록을 보여주는 BannBox과 차단, 차단 해제 시 등장할 Modal 컴포넌트를 자식으로 가진다. 이 부분 역시 fetch 부분을 제외한 채 진행된다.
불리안 타입의 bannModal, cancelModal라는 state를 조작하면서 true일 때 차단하거나 차단해제하는 모달창이 등장하게 한다. 평소에는 false인 state는 form에서 입력을 받거나 X를 눌러 차단 해제를 시도할 때 true가 될 것이다.

BannBox

import { React } from 'react';
import SearchIcon from '@material-ui/icons/Search';

const BannBox = ({ FindCadet, changeInput, input, bannCadet, cancelBann }) => {
  return (
    <div className="bann">
      <div className="head">
        <SearchIcon />

        <form onSubmit={FindCadet}>
          <input
            onChange={changeInput}
            type="text"
            maxLength="12"
            className="search-user"
            value={input}
          />
        </form>
      </div>
      <div className="body">
        {bannCadet.map(name => (
          <div className="banned">
            <text className="banned-id">{name}</text>
            <button
              type="button"
              className="banned-cancel"
              value={name}
              onClick={cancelBann}
            >
              X
            </button>
          </div>
        ))}
      </div>
    </div>
  );
};

export default BannBox;

props으로 form 입력 시 실행될 FindCadet, input 태그의 값이 변경될 시 실행될 changeInput, input 태그의 값을 저장할 input, 차단한 사람들의 배열인 bannCadet, 차단 해제 시 실행될 cancelBann을 받는다.

form으로 입력을 받을 head와 차단한 사용자를 보여줄 body로 구성되어 있다.
head에서 폼으로 인트라 아이디를 입력받아서 제출하면 fetch로 서버에서 해당 사용자를 찾아 차단할 것인지 묻는 모달창을 생성할 것이지만, fetch를 구현하지 않았으므로 무조건 모달창을 생성할 FindCadet 콜백함수를 호출할 것이다.
body에서는 차단된 사용자 배열을 map을 이용해 각각 컴포넌트로 만든다.

import { React } from 'react';

const Modal = ({ close, id, bann, message, type }) => {
  // 열기, 닫기, 모달 헤더 텍스트를 부모로부터 받아옴
  let typeEvent;
  let text;

  const BannEvent = () => {
    // if 해당 아이디를 찾는다 - 존재하면 벤 추가해서 post, 존재하지 않으면 존재하지 않는다.
    // 차단했다면 아이디를 전달해야한다!
    bann(prevBann => [...prevBann, id]);
    close();
  };
  const CancelEvent = () => {
    bann(prevBann => prevBann.filter(e => e !== id));
    close();
  };

  // type에 따라 차단 함수 차단 해제 함수가 분기된다.
  if (type === 'bann') {
    typeEvent = BannEvent;
    text = '차단';
  } else {
    typeEvent = CancelEvent;
    text = '해제';
  }
  const BannNo = () => {
    // 그냥 취소
    close();
  };

  return (
    // 모달이 열릴때 openModal 클래스가 생성된다.
    <div className="modal">
      <section>
        <header>{text}</header>
        <main>
          <text className="id">{id}</text>
          {message}
        </main>
        <footer>
          <button type="button" className="yes" onClick={typeEvent}>
            {text}
          </button>
          <button type="button" className="close" onClick={BannNo}>
            취소
          </button>
        </footer>
      </section>
    </div>
  );
};

export default Modal;

팝업창과 모달창의 차이는 브라우저 위에 새로운 브라우저를 만드느냐, 새로운 레이어를 까냐에 따라 갈린다. 페이지를 이동해도 사라지지 않는 팝업창과는 달리 부모 자식 관계인 모달창은 부모 페이지를 이동하면 사라지게 된다.

부모 컴포넌트에서 두 개의 다른 props를 보내주는데, type에 따라 차단하는 모달창과 차단 해제하는 모달창이 나뉘어 질 것이다. 그것을 위해 리턴되는 jsx 코드의 상당 부분이 state나 props로 되어 있다.

bann은 props로 받은 setBannCadet인데, 차단한 리스트를 재설정하는 함수이다. 누군가를 차단할 때는 기존의 요소에 선택된 아이디를 추가하는 방식으로, 누군가를 차단 해제할 때는 기존의 요소에 선택된 아이디를 제거하는 방식으로 구현하였다.



이슈

scss 중복? 다른 디렉토리고 import도 안했는데?

서로 다른 디렉토리에 있는 scss 파일이, import 하지도 않은 다른 js파일에 영향을 주는 이슈가 발생했다. 동일한 네임스페이스가 scss 파일에서 설정된 경우, 부모 자식 상관없이 해당 네임스페이스를 가지는 모든 HTML 엘리먼트가 영향을 받게 되는 것으로 나타났다. 기준이 무엇인지, 이유가 무엇인지는 모르지만 일단은 scss 파일들에서 중복되는 네임스페이스가 없도록 조치하는 것으로 급한 불을 꺼 두었다.

자식 컴포넌트에서 부모 컴포넌트의 state를 수정할 때


const Apple = (props) => {
 return (
   <div>
   	<h3>등급 : {props.grade} 신선도 : {props.fresh}: {props.delicious}</h3>         {/* delicious라는 키는 없기 때문에 taste에 접근하지 못한다. */}
   	<h3>{props.location}에서 나왔습니다.</h3>
   </div>
   
   )
}

const AppleFarm = () => {
return (
	<Apple
		grade="A++"
		fresh="A"          
		taste="A+"		
		location="충주"
        />
  	)
}

useState를 분할할당해서 나온 set 함수를 props로 넘겨도 제대로 작동하지 않는 상황이 발생했다. 원인은 단순했다. 자식 컴포넌트로 prop를 넘길 때 {propKey}:{propValue} 형태로 전달하게 되는데, 자식 컴포넌트에서 넘길 때와 동일한 propKey를 사용해야 key에 대응하는 propValue를 받을 수 있었던 것이다. 임의로 다른 propKey로 받으려 하니 제대로 받을 수 없었던 것이다.


중요 개념


느낀 점

겨우겨우 전체적인 뼈대 구현에 성공했다. react보다 기초적인 html, css에 대한 지식이 탄탄해야 할 필요성을 절실하게 느낀 시간이었다. fetch 부분과 42 seoul API를 사용하는 부분도 같이 다루려고 하였으나 예상했던 것 보다 더 난관을 겪게 되어서 다음 번에 한꺼번에 다룰 생각이다.

profile
씨앗 개발자

0개의 댓글