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

sham·2021년 8월 27일
0

Bobs 개발일지

목록 보기
2/6
post-thumbnail

현재 상황

MyPage.js

import React from 'react';
import Profile from 'components/Profile';
import MealStat from 'components/MealStat';
import './MyPage.scss';

const MealLog = () => {
  return <div> 한달 동안의 진행되었던 식사들에 대한 데이터</div>;
};
const Bann = () => {
  return <div>밴 리스트 + 밴 해제 팝업창</div>;
};

const MyPage = () => {
  return (
    <>
      <Profile />

      <MealStat />

      <MealLog />

      <Bann />
    </>
  );
};

export default MyPage;

프로필 페이지를 담당하기로 한 후, 지금 생각할 수 있는 큰 과제는 다음과 같았다.

  • 자바스크립트에서 서버로부터 데이터 주고받는 방법 익히기 (fetch, promise, async, await)
  • 받아온 데이터 파싱하기
  • css(혹은 Material UI) 적용해서 디자인

구현

Profile

Profile.js

import React, { useRef, useState } from 'react';
import './Profile.scss';
import defaultProfile from './default.png';

const Profile = () => {
  const [url, setUrl] = useState();
  const [userName, setUserName] = useState('user');
  const imgRef = useRef();

  const ImgOnclick = e => {
    e.preventDefault();
    imgRef.current.click();
  };

  const onImgChange = e => {
    const reader = new FileReader();
    const file = e.target.files[0];

    if (file) {
      reader.onload = () => {
        setUrl(reader.result);
      };
    }
    reader.readAsDataURL(file);

    setUserName('user');
  };

  const image = url || defaultProfile;
  return (
    <div className="container">
      <div>
        <img src={image} className="profile" alt="profile" />

        <button onClick={ImgOnclick} className="profileChange" type="button">
          {' '}
        </button>
      </div>

      <input
        type="file"
        className="profileImgInput"
        id="profileImgInput"
        accept="image/*"
        ref={imgRef}
        onChange={onImgChange}
      />
      <div className="text">{userName}</div>
    </div>
  );
};

export default Profile;

Profile 컴포넌트에서 구현해야 할 것은 다음과 같다.
1. 서버로부터 프로필을 받아서 화면에 띄우기. - 서버가 없는대로 임시로 구현
2. 버튼을 눌러서 프로필을 다른 사진으로 교체할 수 있게끔 하기.

프로필 교체

 const onImgChange = e => {
    const reader = new FileReader();
    const file = e.target.files[0];

    if (file) {
      reader.onload = () => {
        setUrl(reader.result);
      };
    }
    reader.readAsDataURL(file);

    setUserName('user');
  };

사진을 교체하기 위해 FileReader()라는 API를 사용하였다. 로컬내 이미지 주소를 받아와서 image 태그에 집어넣는 방식이다.

input file에서 파일이 선택되면 onchange가 동작하여 해당 파일 객체에 e.target.files로 접근해 file 객체를 얻을 수 있다.

file 객체에 대한 설명은 https://taeny.dev/javascript/file-object/ 참조.

file.onload는 load 이벤트의 핸들러로 load가 끝났을 때 비동기적으로 실행된다. reader.readAsDataURL가 실행되면 reader.result에 해당 이미지의 url이 저장되고, onload의 조건이 충족되어 이미지의 url를 설정하게 된다.

정리하면서 보니 의도했던 동작과 완전히 다르게 작동하고 있다. 서버에서 주고 받는 게 가능하면 전체를 뜯어 고쳐야 할 것 같다...ㅠ


MealStat

MealStat.js

import { React, useState } from 'react';
import MealStats from './MealStats';

const MealStat = () => {
  const [data, setData] = useState(['', '', '', '']);
  /*
    { i: '1' }, // 총 먹은 횟수, 먹은 횟수만 받으면 됨.
    { i: '2' }, // 가장 많이 간 곳, 서초 개포 중 한 곳
    { i: '3' }, // 가장 많이 먹은 메뉴, 최상위 3개 중 랜덤으로 하나 선택, 배열로 저장해놓을까?
    { i: '4' }, // 가장 같이 먹은 사람, 먹은 기록 다 뽑아서 가장 많이 매칭된 사람 선택
  ]);
    */

  if (data.length === 0) {
    setData(prevdata => {
      return [...prevdata, {}];
    });
  }
  return (
    <div className="flex-container">
      {data &&
        data.map((object, i) => {
          return <MealStats info={object.i} code={i} />;
        })}
    </div>
  );
  // 4개의 자식 컴포넌트, 각 컴포넌트는 사용자의 정보에 따라 다른 값을 생성.
  // props을 줄때부터 바로 처리할 수 있도록 state의 배열 안에 해당되는 정보를 세팅해놔야 한다.
};

export default MealStat;

MealStats.js

import React from 'react';
import './MealStat.scss';
// import example from './default.png';
import Avatar from '@material-ui/core/Avatar';
import ApartmentIcon from '@material-ui/icons/Apartment';
import RestaurantMenuIcon from '@material-ui/icons/RestaurantMenu';
import AssignmentIcon from '@material-ui/icons/Assignment';
import GroupIcon from '@material-ui/icons/Group';
import { makeStyles } from '@material-ui/core/styles';

const useStyles = makeStyles(theme => ({
  avatar: {
    margin: theme.spacing(2),
    width: 70,
    height: 70,
    backgroundColor: 'white',
    alignItems: 'center',
  },
  icon: {
    color: '#15b2b3',
    fontSize: 40,
  },
}));

const MealStats = ({ info, code }) => {
  let comment;
  let icon;
  const classes = useStyles();
  const parseCode = () => {
    switch (code) {
      case 0: {
        comment = `지금까지 총 ${info}번 식사를 하셨네요!`;
        icon = <ApartmentIcon className={classes.icon} />;
        break;
      }
      case 1: {
        comment = `${info}를 주로 
		방문하셨네요!`;
        icon = <RestaurantMenuIcon className={classes.icon} />;
        break;
      }
      case 2: {
        comment = `${info}를 사랑하는 당신은 ${info} 매니아!`;
        icon = <AssignmentIcon className={classes.icon} />;
        break;
      }
      case 3: {
        comment = `당신의 최고의 밥 친구는
		${info}입니다!`;
        icon = <GroupIcon className={classes.icon} />;

        break;
      }
      default:
    }

    return (
      <div className="flex-box">
        <Avatar className={classes.avatar}>{icon}</Avatar>
        <p className="text content">{comment}</p>
      </div>
    );
  };
  return <>{parseCode()}</>;
};

export default MealStats;

MealStat 컴포넌트에서 구현해야 할 것은 다음과 같다.
1. 서버로부터 사용자에 대한 데이터를 받아오기.
2. 받아온 데이터들을 파싱해서 props으로 넘겨줄 데이터를 추출하기.
최종적으로는 [총 식사한 횟수, 가장 많이 간 장소, {특정 메뉴, 먹은 횟수}, 가장 많이 밥 먹은 동료] 형태가 될 것이다.
자식 컴포넌트에서 prop를 바로 써먹을 수 있을 만큼 완벽하게 파싱해야만 한다.
3. 추출한 데이터를 map을 사용해서 MealStats 컴포넌트로 넘겨주기. 총 4개의 자식 컴포넌트를 가진다.

MealStat 컴포넌트 역시 서버에서 데이터를 받아오는 게 핵심이기에 뼈대를 구현하는 것이 그쳤다. 로직적으로도 특별히 기술할만한 부분은 없었다.


이슈

리액트보다는 scss 때문에 생긴 문제가 훨씬 많았다. 기본기 부족에 대한 업보가 이렇게도 빠르게 찾아올 줄이야...OTL

react - is missing in props validation react/prop-types


부모 컴포넌트로부터 받은 props를 사용하려고 했을 때 위와 같은 오류가 발생하였다. 팀의 프론트 고수분께 여쭈어 본 결과 eslint 때문에 생긴 오류일 수 있다며 알려주신 npm install --save prop-types 를 터미널에 입력하니 감쪽같이 해결되었다.

scss - 겹치게 배치


  .profileChange {
    background-color: white;
    border: 0;
    background: url('./cameraIcon.png');

    position: absolute;
    width: 35px;
    height: 35px;
    @include circle;
    cursor: pointer;
    top: 5%;
    right: 46%;
  }

맨 처음 계획은 위의 사진과 같이 프로필 화면과 수정 버튼을 겹치게 배치할 생각이었다. 그를 위해 css의 position 속성을 absolute 바꿔주고 right 값을 줘서 겹치게 해주었으나 화면의 크기가 변함에 따라 수정 버튼도 이동하게 되는 것이었다. css 단에서 충분히 해결할 수 있는 사안 같아서 일단 보류하고 있다. 영 안되면 그냥 떨어트려놔도 되니...

고수님의 도움으로 쉽게 해결할 수 있었다. 기본적으로 position:absolute 속성은 부모 엘리먼트 내부에 속박되지 않고 독립된 영역, 새로운 레이어를 가지게 된다. 이 때 transform: translate({x축}, {y축});으로 해당 엘리먼트를 절대적인 방식으로 이동시키면 손쉽게 부모 태그 위에 배치할 수 있게 된다.

absolute 속성에 대한 자세한 설명은 https://www.daleseo.com/css-position-absolute/ 참조

scss - 부모 자식 태그

.conatainer {
	width : 100px;
	.box {
    	height : 50px;
    }
    .text {
    	height : 10px;
    }
}

다음과 같은 코드에서 부모 태그에 적용된 구문은 자식 태그에도 적용될 것이라고 생각했으나, 저 코드의 의미는 .container 안에 포함되어 있는 .box 선택자의 유효범위를 나타낸 것이었다.


중요 개념

scss - block, inline

block : 한 영역을 차지하는 박스 형태의 요소. 정확히 말하면 가로, width를 꽉 차지한다. 자신에게 주어진 영역(컨텐츠(width))를 무시하고 한 줄을 통째로 차지한다. 그렇기에 전후 줄바꿈이 일어나게 된다. 대표적으로 div가 있다.
height, width, margin, padding을 지정할 수 있다.

inline : 자신에게 주어진 영역(컨텐츠)만큼만 차지하는 요소. 한 줄에 여러 inline 태그가 존재할 수 있다. width, height값이 컨텐츠 영역만큼 자동으로 설정된다. 대표적으로 span이 있다.
height, width를 임의로 설정 할 수 없으며 지정한들 무시된다. margin, padding은 좌우만 적용된다.

inline-block : inline의 특징과 block의 특징을 모두 가진 요소.

줄바꿈이 이루어지지 않는다.
block처럼 width와 height를 지정 할 수 있다.
만약 width와 height를 지정하지 않을 경우, inline과 같이 컨텐츠만큼 영역이 잡힌다.

상기한 요소들 전부 display 속성으로 설정해 줄 수 있다.

material ui

링크


import PersonIcon from '@material-ui/icons/Person';
import { makeStyles } from '@material-ui/core/styles';

const customStyles = makeStyles(() => ({
  icon: {
    color: '#15b2b3',
    fontSize: 40,
  },
}));

const MealLog = () => {
    const classes = customStyles();

  return (
    <PersonIcon className={classes.icon}/>
  );
};

리액트 개발에서 쉽게 사용할 수 있는 UI Framework.
이미 만들어진 템플릿이나 컴포넌트를 가져다 쓸 수 있지만, 해당 컴포넌트에 css를 적용하려면 기존의 방식인 css에서 설정하는 게 아닌 makeStyles() 함수를 사용해야만 한다.
.class이름으로 class를 적용해서 함수에서 객체처럼 설정해준다.
특이한 점으로 속성들은 css의 그것과 동일하지만 css의 표기법인 케밥 케이스가 아닌 javascript의 카멜 케이스로 표기한다.

느낀 점

배영만 배웠는데 한강 종주를 하게 된 기분이다. 다른 사람의 발목을 붙잡고 싶지 않다는 심정으로 닥치는 대로 찾아보면서 하고 있다. 그래도 확실히 클론코딩 하는 것보다는 실력이 많이 늘고 있는 것 같다.

profile
씨앗 개발자

0개의 댓글