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

sham·2021년 10월 10일
0

Bobs 개발일지

목록 보기
6/6
post-thumbnail

현재 상황

나의 작업은 일단 일단락된 상태로 일주일 가량이 지났다. 그러나 이렇게 가만히 있을 수는 없다. 깨작깨작 만져본 것들도 있겠다, 이노베이션 아카데이의 기라성같은 멘토님들과 과제를 혼자 캐리해가시는 초절정 고수 동료 분께 멘토링을 신청해서 코드에 대한 리뷰도 받았겠다, 뭐라도 적어보자.

수정한 것

  • 브랜치 간 깃 로그 맞추기.

남은 과제

  • Material UI 에서 아이콘 집어넣기
  • 서버와의 통신(서버가 돌아가야 가능)

깃 로그 맞추기


수정

Bann.js

state를 줄이기

state가 변하면 re-render가 되는데, react의 최적화는 일단 re-render를 줄이는 것. Bann.js 에서 state를 줄일 수 있을까?

컨벤션 맞추기 (이름 규격화)

이벤트 리스너에 등록하는 함수명은 handle~ 로 시작하는게 보통의 컨벤션.

코드를 다시 살펴보니 함수의 이름이 제멋대로 지정되어 있었다.
이벤트 리스너로 들어가는 함수의 경우 handle + 역할 + (필요하면 역할의 대상) 형식으로 수정해 주고, prop로 주게 되었을 때는 html 태그처럼 on을 붙어주었다.

자바스크립트의 함수는 보통의 컨벤션에 따라 소문자로 시작해주는 것이 적절.

리액트에서 첫글자가 대문자인 경우는 컴포넌트임을 의미하므로 소문자로 바꿔준다.

컴포넌트에 key 설정해주기

map을 이용해 요소를 렌더링할 때 key 관련 워닝이 뜬다면 banned 클래스가 달려있는 가장 바깥 div에 key로 고유값을 달아주어야 하는데, map의 index로 달면 안 됨. (관련 레퍼런스: https://ko.reactjs.org/docs/lists-and-keys.html)

key가 필요한 이유에 대해서는 다음 링크링크

그러고보니 map으로 렌더링을 할 때마다 협박하는 듯이 콘솔창을 빨갛게 물들이고 있었다. 목업 데이터를 수정해서 key값을 넣어주자.


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

  const [log, setLog] = useState([
    { id: 0, member: ['asdf', 'qwer'], date: 4 },
    { id: 1, member: ['asdf', 'qwer', 'asfqqd'], date: 7 },
    { id: 2, member: ['asdf', 'qwer', 'zcvsdfw', 'wgezb'], date: 8 },
    { id: 3, member: ['asdf', 'qwer', 'vzre'], date: 12 },
    { id: 4, member: ['asdf', 'qwer', 'zzzz', 'vcxk'], date: 14 },
    { id: 5, member: ['asdf', 'qwer', 'qtw'], date: 18 },
    { id: 6, member: ['asdf', 'qwer', 'erewa'], date: 19 },
    { id: 7, member: ['asdf', 'xzvqw'], date: 21 },
    { id: 8, member: ['asdf', 'qwer'], date: 25 },
  ]);

  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 className="meal-time">
              {log.map(() => {
                return <RestaurantIcon className={classes.icon} />;
              })}
            </div>
          </div>
          <div className="body">
            {log.map(object => (
              <Group key={object.id} data={object.member} date={object.date} />
            ))}
          </div>
        </div>
      </div>
    </div>
  );
};

map을 사용하는 다른 코드들도 같은 처리를 해주었다.

컴포넌트가 아닌 map의 경우

map으로 컴포넌트가 아닌 jsx를 이용한 html 태그를 리턴하게 하는 코드가 존재한다.


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>
      ))}
      <span className="group-date">{date}</span>
    </span>
  );
};

map으로 만들어지는 Group 자식 컴포넌트에서 같이 먹은 사람들의 배열과 먹은 날짜를 props으로 받고 있다. 이 때 위처럼 map을 이용해서 리액트 컴포넌트가 아닌 html 엘리먼트를 만들었을 때도 에러를 뿜나 확인해보니

뿜는다. 다만 이걸 위해 또 컴포넌트화 시켰더니 css가 어그러져서 일단 보류하기로...

키값으로 index를 사용하면 안되는 이유?

map 함수를 사용할 때 두 번째 인자는 해당 배열의 첫 번째 인자값의 인덱스를 나타내고 있다. 이를 이용해 key값도 아싸리 넣어주면 되지 않나 생각했는데, 그러면 안된다고 한다.

URI 인코딩

쿼리에서 오타가 한개만 나와도 에러 발생을 알아채지 못함. URI 인코딩 작업도 필요하다.

각 내용들을 분리해주는 건 물론 인코딩 작업이 필요하다는 조언을 들었다.
인코딩은 사람이 인지할 수 있는 데이터를 다른 형식으로 변환한다는 뜻. URI 인코딩은 URI에서 사용할 수 없는 문자열을 변환하는 자바스크립트 프로세스이다. 한국어를 사용하려고 한다면 URI 인코딩이 필수적이다.
encodeURI, decodeURI 함수를 이용해서 이를 코드화, 복호화할 수 있다.

환경변수 등록

client id, client secret 깃헙에 안 올라가게 - react, env, enviroment variable

42 API와 통신을 하면서 사용하는 코드는 누구에게도 보여서는 안되는 코드이다. 사용자에게서 보이면 안되고 깃허브에도 올라가서는 안되는데, 환경 변수에 등록함으로써 이를 해결할 수 있다고 한다.

// config.js
export const ES_CODE = 'asf34ieaihf309hg';
export const ES_INDEX_NAME = 'img_text_data';
export const ES_DOC_TYPE = '_doc'

// main.js

import * as config from './config';

// ..

function someMethod(){
	const code = config.ES_CODE;	// 이렇게 사용
}

간단하게 위의 방법으로도 가능하지만, 개발환경/배포환경에 따라 다른 변수가 필요할 때는 다른 방법을 사용하게 된다.

// .env.development
REACT_APP_ES_CODE = 'qwradsfadsgadsg';

// main.js

// ..

<p>{process.env.REACT_APP_ES_CODE}<p/>

링크서 발췌

dotenv라는 패키지를 설치해야 .env 파일에 .process.env. 로 접근할 수 있다. 라이브러리 없이도 되겠지 싶어서 그냥 실행하니 하나밖에 접근을 못하고 변수에 "", ;가 덕지덕지 붙어있는 등 난리부루스였다.

create-react-app으로 프로젝트를 만들었다는 전제하에, 루트 디렉토리에 .env.development.env.production를 만든 후 환경 변수를 집어넣어주면 된다. .env.development는 npm start, .env.production는 npm build 에 해당한다.
파일에서 export 해주지 않아도 process.env.로 접근해서 실행할 수 있다. 변수 앞에 REACT_APP_를 붙여야 한다는 것이 유의할 점.
.gitignore 파일에 추가해주는 것을 잊지 말자라고 하려고 했는데...?

형이 왜 거기서 나와?

자세한 설명은 링크

이미 .gitignore에 뒤에만 살짝 다른 .env.development.local .env.production.local 이 포함되어 있었다! 검색해보니 CRA로 만든 프로젝트는 전부 .gitignore에 해당 파일이 제외되어 있는 것으로 확인되었다. .local의 의미가 무엇인가 검색해보니, .local이 붙은 것들은 안 붙은 것들을 덮어쓰는, 우선순위가 높은 파일이라고 하더라. 우선순위를 구분할 만큼 환경변수를 많이 쓸 생각은 없었기에, 우리가 만든 파일들을 추가해주는 선에서 마무리했다.

try catch 분할화

try catch로 잡아낼 때는 하나하나의 상황을 분리해서 대응해야 할 수도 있다.


이슈

State 변경에 따른 토글 제어하기

방에 참가한 사람들의 아이콘을 클릭하면 하이라이트 되며 인트라 아이디가 밑에 떠오르는 것을 구현하고 싶었다.
처음에는 방의 참가인원만큼의 배열을 만들고 불리안으로 클릭되면 true로 만들어서 map으로 뿌릴 때 true인 요소를 하이라이트, 아이디 처리해주려고 했다.
useRef로 상태 관리를 하니 리렌더링이 되지 않아 변경사항이 적용되지 않았다.
state를 써서 배열의 값을 바꿔도 리렌더링이 되지 않았다.
알아보니, state가 객체라면 useState로 값을 수정한다고 해도 객체의 주소가 변하지 않을 시 state는 변했다고 판정을 하지 않는다고 한다.

const A = ["A", "A", "A"]
const [test, setTest] = useSate(A);
const temp = A;
temp[1] = "B";
setTest(temp);

즉, 위와 같은 코드에서 setTest를 해도 temp 역시 A주소를 가리키고 있기 때문에 state는 변하지 않는 것이다.
state가 객체일 때 값을 변경하면서 리렌더링을 하고 싶다면 ... 문법을 이용하면 된다.
...{배열} 시 해당 배열의 모든 요소를 꺼내준다.
setTest([...temp])setTest(["A", "B", "A"])와 동일하다. 새로운 배열을 만든 것이기 때문에 주소 역시 다르고, 리렌더링도 정상적으로 작동한다.

  useEffect(() => {
    console.log('state change');
  }, [toggleState]);

  const hangleToggle = e => {
    if (toggleState[e.target.alt]) {
      console.log('same!');
      setToggleState([...basicState]);
      return;
    }
    const temp = basicState;
    temp[e.target.alt] = temp[e.target.alt] === false;
    setToggleState(prevState => {
      console.log(prevState);
      return [...temp];
    });
  };
return (
 <div className="group">
          {member.map((e, i) => {
            return (
              <div className="group-person">
                <img
                  className="group-person-profile"
                  alt={i}
                  src="assets/dummyPerson.jpg"
                  onClick={hangleToggle}
                  role="presentation"
                />
                {toggleState[i] === true && (
                  <>
                    <div
                      className="group-person-profile-focus"
                      onClick={hangleToggle}
                      role="presentation"
                    >
                      {}
                    </div>
                    {}
                    <text className="group-person-id">{e}</text>
                  </>
                )}
              </div>
            );
          })}
        </div>
)

리렌더링이 정상적으로 작동하는 것을 확인했다.

onMouseOver

 <img
                    className="group-person-profile"
                    alt={e}
                    src="https://cdn.icon-icons.com/icons2/1904/PNG/512/profile_121261.png"
                    onMouseOver={changeStyle}
                    onMouseOut={changeStyle}
                    onFocus=""
                    onBlur=""
                  />

Non-interactive elements should not be assigned mouse or keyboard event listeners

img element에 onClick을 부여하니 발생했다.


    <img
                  className="group-person-profile"
                  alt={i}
                  src="assets/dummyPerson.jpg"
                  onClick={hangleToggle}
                  role="presentation"
                />

Div에 이벤트 넣으려 했을 때도 비슷한 증상이 발견되었는데, 이를 완벽하게 해결할 수 있는 방법을알아냈다.
role="presentation" 을 넣어주면 ESLint 오류가 말끔하게 사라지게 된다!



혹시나 ESLint가 없이도 생기는 에러인가 싶어서 실험을 해보았는데 그런 건 아니었다.
ESLint에서는 div, img 같은 정적인 태그들에 이벤트를 붙이는 것을 잘못된 문법이라고 판단해서 생긴 이슈인 것으로 결론을 내린다.
role이 무슨 역할을 하기에 에러를 잡아준 건지는 모르겠지만;;^^

role에 관한 더 자세한 내용은 다음 링크로.


중요 개념

무한 스크롤

디자인 패턴

URI 인코딩

환경 변수

socket IO

웹소켓에 대한 자세한 내용은 다음 링크로
https://hees-dev.tistory.com/53
https://urmaru.com/7
https://ko.javascript.info/websocket

기존의 HTTP 통신은 요청이 없다면 클라이언트와 서버는 단절된 상태였고, 클라이언트가 서버가 데이터를 요청하면 응답이 도착하기까지 기다려야만 했다.
새롭게 등장한 웹소켓 통신을 이용하면 서버와 클라이언트가 특정 포트로 연결되어 연결을 계속 유지한 상태가 되었고, 데이터도 실시간으로 주고 받는 것이 가능해진다.

그렇다면 socket.io는? 당근 웹소켓을 쉽게 사용할 수 있게 해주는 모듈이다. npm install --save socket.io로 설치할 수 있다. 그럼 실제 코드를 보면서 확인해보자.

// server.js
const express = require('express');
const socketIo = require('socket.io');
const http = require('http');

const app = express();
app.set('port', process.env.PORT || 5000);

app.use(require('./routes/index'));

const server = http.createServer(app);
const io = socketIo(server);

const socketList = [];

io.on('connection', function (socket) {
  socketList.push(socket);
  console.log('User Join');

  socket.on('SEND_MESSAGE', function (msg) {
    socketList.forEach(function (item) {
      console.log(item.id);
      if (item !== socket) {
        item.emit('RECEIVE_MESSAGE', msg);
      }
    });
  });

  socket.on('disconnect', function () {
    socketList.splice(socketList.indexOf(socket), 1);
    console.log('User Out');
  });
});

server.listen(5000, function () {
  console.log('Server On !');
});

//Chat.js
import React, { useState, useRef, useEffect } from 'react';
import io from 'socket.io-client';
import propTypes from 'prop-types';
import ChatLogContainer from './ChatLogContainer';
import ChatInput from './ChatInput';
import ChatHeader from './ChatHeader';

let socket;

const Chat = props => {
  const [chatLogs, setChatLogs] = useState([]);
  const { showModal, userName } = props;
  const bottomRef = useRef();

  useEffect(() => {
    socket = io('localhost:5000', { transports: ['websocket'] });
    console.log('socket ::', socket);

    socket.on(
      'RECEIVE_MESSAGE',
      msgInfo => {
        setChatLogs(preState => [
          ...preState,
          { isMyMessage: false, ...msgInfo },
        ]);
      },
      [],
    );

    return () => {
      socket.emit('disconnect');
    };
  }, []);

  const moveScrollBottom = () => {
    const top = true;
    console.log('change');
    bottomRef.current.scrollIntoView(top);
  };

  useEffect(() => {
    moveScrollBottom();
  });

  const sendMessage = msg => {
    const date = new Date();
    const currentTime = date.toString();
    const msgInfo = {
      id: chatLogs.length + 1,
      author: userName,
      message: msg,
      time: currentTime,
    };
    socket.emit('SEND_MESSAGE', msgInfo);
    setChatLogs(preState => [...preState, { isMyMessage: true, ...msgInfo }]);
    moveScrollBottom();
  };

  return (
    <>
      <main className="chat">
        <ChatHeader showModal={showModal} roomName="재밌는 방" />
        <ChatLogContainer chatLogs={chatLogs} bottomRef={bottomRef} />
        <ChatInput callback={sendMessage} onMoveScroll={moveScrollBottom} />
      </main>
    </>
  );
};

Chat.propTypes = {
  userName: propTypes.string.isRequired,
  showModal: propTypes.func.isRequired,
};

export default Chat;

emit은 데이터 전송, on은 데이터를 받는다는 것을 기억하자!
위의 코드는 각각 서버, 클라이언트를 나타낸다. express를 모르는 내가 봐도 server.js에서는 5000번 포트에 서버를 돌리고 있고, Chat.js에서는 웹소켓 모듈을 해당 포트와 연결하고 있다. 모듈을 불러올 때 역할에 따라 각각 다른 모듈을 불러오는 것이 눈에 띈다. (socket.io, socket.io-client)

자동 스크롤(화면 전환)

https://mine-it-record.tistory.com/399
https://webisfree.com/2021-08-19/특정-엘리먼트로-스크롤을-천천히-이동시키는-방법-scrollintoview-smooth
https://developer.mozilla.org/ko/docs/Web/API/Element/scrollIntoView

element.scrollIntoView();
element.scrollIntoView(alignToTop); // Boolean parameter
element.scrollIntoView(scrollIntoViewOptions); // Object parameter

HTML 엘리먼트의 내장 메서드. 스크롤을 제어할 수 있다.
세 가지 방법으로 사용 가능.
1. 파라미터 없이 사용.

  • 아무 옵션 없이 아래로 스크롤된다.
  1. 불리안 파라미터 사용.
  • true : element 요소의 상단을 기준으로 스크롤을 이동한다.
  • false : element 요소의 하단을 기준으로 스크롤을 이동한다.
  1. 객체 파라미터 사용.
  • behavior : 전환 에니메이션 정의 (auto, smooth)
  • block : 수직 정렬 (start, center, end, nearest)
  • inline : 수평 정렬 (start, center, end, nearest)

느낀 점

배워야 할 것들이 아직도 많다. 항상 자신을 다잡자.

profile
씨앗 개발자

0개의 댓글