[슬랙 클론코딩] 4주차 회고록

jung moon chai·2022년 10월 31일
0

1. 재사용 가능한 컴포넌트와 props

1-1. Hooks와 Props

리액트의 상태관리 라이브러리는 많은 툴들이 있다. 대표적으로 redux, mobx같은 툴을 예로 들 수 있는데, 이와 맞물려 훅스가 나오게 되면서 자식 컴포넌트 자체에서 데이터를 가져올 수 있게 되어 리액트 개발의 디자인패턴이 많이 바뀌게 되었다.
훅스가 나오기 이전에는 부모컴포넌트가 자식컴포넌트에게 props로 데이터를 전달하고, 부모의 컴포넌트에서 state가 바뀔때 자식컴포넌트의 데이터가 바뀌지 않았는데도 리렌더링되는 것을 방지하기 위해 memo등으로 최적화를 하고, 이를 위해 컴포넌트 개발 방식도 뷰만을 담당하는 컴포넌트와 데이터만 다루는 컨테이너 컴포넌트 방식으로 나뉘어져 있었다. 하지만 훅스를 통해 자식컴포넌트가 직접 데이터와 통신해 상태관리를 하여 더 이상 컨테이너 컴포넌트 개발패턴은 잘 사용되지 않게 되었다.


1-2. Props는 더 이상 필요 없을까?

결론부터 말하자면 그렇지 않다. 재사용 가능한 컴포넌트를 만들기 위해서는 props는 여전히 필요하다.

import { VFC, useCallback } from 'react';

interface Props {
  chat: string;
}

const ChatBox: VFC<Props> = ({ chat }) => {
	const onSubmitForm = useCallback((e) => {
    	e.preventDefault();
      	// ...DM보내기
    }, []);
	return (
    	<form
      		onSubmit={onSubmitForm}
      	>
      		<button
      			type="submit"
      		></button>
      	</form>
    )
}

메세지를 작성해 보내는 폼 컴포넌트가 있다. 슬랙을 보면 DM에서도 메세지 폼이 있고,
채널에서도 메세지 폼이 있다. 하지만 onSubmit을 컴포넌트 안에 DM을 보내는 api요청을 만들게 되면 채널 페이지에서는 해당 컴포넌트를 사용 할 수 없다. 그렇기 때문에 재사용하지만 서로 다른 데이터들의 디테일한 작업은 부모 컴포넌트에서 만들어 Props로 넘겨 주어야 재사용 가능한 컴포넌트를 만들 수 있다.

// /components/ChatBox/index.tsx
import { VFC, useCallback } from 'react';

interface Props {
  chat: string;
  onSubmitForm: (e: any) => void;
}

const ChatBox: VFC<Props> = ({ chat, onSubmitForm }) => {
	return (
    	<form
      		onSubmit={onSubmitForm}
      	>
      		<button
      			type="submit"
      		></button>
      	</form>
    )
}
// /pages/Channel/index.tsx
import { useCallback } from 'react';
import ChatBox from '@components/ChatBox';

const Channel = () => {
	const onSubmitForm = useCallback((e) => {
  		e.preventDefault();
      	// 채널 메세지 보내기
    }, []);
  
    return (
    	<div>
      		<ChatBox onSubmitForm={onSubmitForm} />
      	</div>
    )
}

// /pages/DirectMessage/index.tsx
import { useCallback } from 'react';
import ChatBox from '@components/ChatBox';

const DirectMessage = () => {
	const onSubmitForm = useCallback((e) => {
  		e.preventDefault();
      	// 채널 메세지 보내기
    }, []);
  
    return (
    	<div>
      		<ChatBox onSubmitForm={onSubmitForm} />
      	</div>
    )
}

2. WebSocket

2-1. Socket.io

웹소켓은 실시간으로 서버와 데이터를 주고 받는데 사용되는 기술로, 단방향 통신을 양방향 통신으로 사용이 가능해졌다. 웹소켓이 없다면 주기적으로 데이터를 요청하여 데이터가 추가 된 것이 있는지 계속 확인하는 polling(폴링)방식으로 개발해야 한다. 약간 실시간 데이터처리와는 동떨어져있지만 푸시같은 기능을 만들때 SERVER SENT EVENT 라는 기술도 있다.
SSE와 SOCKET의 차이


2-2. socket.io 전용 Hooks

프론트에서 socket을 백엔드와 소켓을 연결하기 위해 소켓 라이브러리를 설치해 주자.

$ npm i socket.io-client@2 
$ npm i -D @types/socket.io-client 

socket.io-client는 3버전 까지 나와있지만 강좌의 백엔드에서 사용하는 nestjs와 typeORM이 아직 3버전을 지원하지 않아 2버전을 사용하였다. 2버전에 비해 3버전에서 추가 개선사항들이 있지만 크리티컬한 문제는 아니기에 현재까지도 2버전을 사용하는데 큰 무리는 없다.

const socket = io.connect(`${backUrl}`);
socket.emit('hello', 'world');
socket.on('message', ({ data }) => {
	console.log(data);
});
socket.disconnect();

const socket = io.connect(`${backUrl}`); connect함수를 사용해 소켓서버와 연결하고,

socket.emit('hello', 'world'); emit함수를 활용해 hello라는 이벤트이름으로 world라는 데이터를 보내게 된다.
socket.on('message', () => {...}) on함수는 서버측에서 데이터가 넘어오면 message라는 이름에다 데이터를 받는 콜백함수를 작성 할 수 있다.

connect : 소켓서버와 연결
emit : 클라이언트에서 서버로 데이터를 보냄 (이벤트명, 데이터)
on : 서버에서 클라이언트로 보내는 이벤트 (이벤트명, 콜백함수)
off : 이벤트명에 대한 리스너를 제거 (이벤트명)
disconnect : 소켓서버와의 연결을 끊음

소켓을 사용할 때 주의 할 점은 통신할 범위 를 조절 해줘야 하고,
슬랙은 각 채널, dm 처럼 연결 할 대상이 채팅처럼 나눠져 있기때문에 해당 채널이나 dm을 벗어나게 되면 disconnect해주어야 한다. 연결을 끊어주지 않는다면 다른 채널 및 dm으로 옴겼을때 끊지 않은 모든 채팅데이터들이 업데이트 된다.

그렇다면 이제 훅스로 만들어보자.

import io from 'socket.io-client';
import { useCallback } from 'react';

const backUrl = 'http://localhost:3095';
const sockets: { [key: string]: SocketIOClient.Socket } = {};

const useSocket = (workspace?: string): [SocketIOClient.Socket | undefined, () => void] => {
  const disconnect = useCallback(() => {
    if (workspace) {
      sockets[workspace].disconnect();
      delete sockets[workspace];
    }
  }, []);

  if (!workspace) {
    return [undefined, disconnect];
  }
  if (!sockets[workspace]) {
    sockets[workspace] = io.connect(`${backUrl}/ws-${workspace}`, {
      transports: ['websocket'],
    });
  }
  return [sockets[workspace], disconnect];
};

export default useSocket;

2-3. receiveBuffer, sendBuffer, callbacks

receiveBuffer : 데이터를 받아야 하는데 못받을 때 서버에서 쌓아놨다가 서버가 연결되면 전달해 준다.
sendBuffer : 데이터를 보내야 하는데 서버와 연결이 끊겨 데이터가 가지 않을 때 쌓였다가, 서버가 다시 연결되면 서버로 쌓여있던 데이터를 한번에 다시 보내준다.
callbacks : on으로 연결 했던 이벤트들이 쌓여있다. (connect, conntecting은 기본으로 들어있음)


3. 정규표현식

정규 표현식은 문자열에서 특정한 문자조합을 찾기위한 패턴이다.

\d : 숫자
+ : 1개이상
? : 0개 나 1개
* : 0개 이상
g : 모두찾기
\ : 이스케이프 특수기호 무력화
. : 모든글자
\n : 줄바꿈
| : 또는
a(b)c : 전체 패턴을 검색한 후에 괄호 안에 명시된 문자열을 저장 (ex : “abc"를 검색한 후에 b를 저장)
[abc] : 꺾쇠 괄호([]) 안에 명시된 문자를 검색 (ex : “abc"를 검색함.)
[0-3] : 꺾쇠 괄호([]) 안에 명시된 숫자를 검색 (ex : 0부터 3까지의 숫자를 검색함.)
[\b] : 백스페이스 문자를 검색
{n} : 앞의 문자가 정확히 n번 나타나는 경우를 검색
{m,n} : 앞의 문자가 최소 m번 이상 최대 n번 이하로 나타나는 경우를 검색

정규표현식에서 괄호들은 특수한 기능을 하고 있는데 찾으려는 문자열에 괄호가 있는경우 \를 사용해 해당 괄호를 문자열로 기능을 무력화 시켜주어야한다.


4. 리액트 컴포넌트 렌더링 최적화

4-1. React.memo

자식컴포넌트를 React.memo로 컴포넌트를 감싸주어 부모컴포넌트는 바뀌어도 자식컴포넌트의 props가 바뀌지 않았다면 리렌더링 하지 않는다.

import React, { useState } from 'react';

const ChildComponent = ({ childData }) => {
	return (
    	<div>
      		{childData ? 'true' : 'false'}
      	</div>
    )
};
const ParentsComponent = () => {
	const [ parentData, setParentData ] = useState(false);
  	const [ childData, setChildData ] = useState(false);
  	return (
    	<div>
      		{parentData ? 'true' : 'false'}
      		<ChildComponent 
      			childData={childData}
      		/>
            <button
				onClick={() => setParentData(!parentData)}       
            >부모컴포넌트 데이터 변경</button>
      	</div>
    )
};

위와 같은 코드의 경우 버튼을 클릭했을때 childData는 전혀 변경이 없고, parentData만 업데이트 된다. 하지만 부모컴포넌트가 리렌더링 될때 자식컴포넌트 또한 같이 리렌더링 되는데 이때 자식컴포넌트를 memo로 감싸주게 되면 부모컴포넌트가 리렌더링 되더라도 자식컴포넌트는 리렌더링 되지 않는다.

import React, { useState, memo } from 'react';
// React.memo로 캐싱
const ChildComponent = memo(({ childData }) => {
	return (
    	<div>
      		{childData ? 'true' : 'false'}
      	</div>
    )
});
const ParentsComponent = () => {
	const [ parentData, setParentData ] = useState(false);
  	const [ childData, setChildData ] = useState(false);
  	return (
    	<div>
      		{parentData ? 'true' : 'false'}
      		<ChildComponent 
      			childData={childData}
      		/>
            <button
				onClick={() => setParentData(!parentData)}       
            >부모컴포넌트 데이터 변경</button>
      	</div>
    )
};

4-2. useMemo

훅스 내에서 개별적으로 데이터를 캐싱할 때 사용

import { useMemo } from 'react';
const Component = ({ data }) => {
  	const memo = useMemo(() => {
    	// data props가 업데이트 되었을 때 사용 할 콜백함수
    }, [data]);
	return (
    	<div>
      		...
      	</div>
    )
};

5. 불변성

자바스크립트의 객체나 배열 함수 등은 참조 자료형으로써 기존의 객체를 새로운 변수에 선언을 하게되면 해당 값들은 갖지만 객체는 할당한 변수가 기억하는 메모리 주소를 통해 접근한다.

var a = [
  '제로초',
  '슬랙 클론코딩'
];
var b = a;
b[0] = 'test3';
console.log(a[0]); // test3

a변수의 배열을 b변수에 선언하고 b변수 안에 있는 값을 변경하고 a변수를 콘솔로 찍어보니 b변수에서 바꾼 값이 출력 된다.
b배열이 a배열을 참조 하고 있기 때문에 원본 배열인 a배열이 바뀌게 된 것이다.
상태를 확인 하기 위해서는 원본데이터는 그대로 두어야 비교가 가능한데 그대로 바뀌게 되면 비교를 할 수가 없다.

위와 경우를 객체의 얕은 복사라 하며, 참조상태를 끊어내고 새로운 객체를 복사 하는 것을 깊은 복사라 한다.

5-1. 깊은복사 (deep copy)

var a = [
  '제로초',
  '슬랙 클론코딩'
];
var b = [...a];
b[0] = 'test3';
console.log(a[0]); // 제로초
console.log(b[0]); // test3

위의 경우 1차원 배열에서는 새로운 객체를 만들어 내지만 배열안에 객체가 있는 다중객체일 경우 완벽하게 복사 하지 못하는 경우가 있다.

var a = [
  {id: 1, name: 'test1'},
  {id: 2, name: 'test2'}
]
var b = [...a];
b[0].name = 'test3';
console.log(a[0].name) // test3

스프레드로 복사 하였음에도 원본 객체의 값이 바뀌었다. 1차 객체는 참조가 끊어졌지만 그 안의 객체들이 참조가 끊어지지 않았기 때문이다.

그렇기 때문에 스프레드 문법도 완벽하게 깊은 복사를 해냈다고 볼 수는 없다.

또 다른 방법은 객체를 문자열로 바꾼 후 다시 객체로 바꾸는 것이다.

var a = [
  {id: 1, name: 'test1'},
  {id: 2, name: 'test2'}
]
var b = JSON.parse(JSON.stringify(a))

a[0] === b[0] // false
a === b // false


간편한 방법이지만 매우 속도가 느리다.
가장 쉽게 새로운 객체로 복사하는 방법은 lodash 라이브러리를 사용하는 것이다.

var a = [
  {id: 1, name: 'test1'},
  {id: 2, name: 'test2'}
]
var b = _.cloneDeep(a);

a[0] === b[0] // false
a === b // false

lodash는 해당 기능 말고도 빈 객체를 확인하는 등의 여러 기능을 제공하고 있다.


5. 인피니티 스크롤링

강좌에서 사용 하고 있는 swr은 인피니티 스크롤링 전용 매서드를 지원하고 있다.

const {
    data,
    mutate,
    revalidate,
    setSize,
  } = useSWRInfinite<IDM[]>(
    (index) => `api 주소`, // 첫번째 매개변수가 useSWR의 url이 아닌 함수 형태로 교체되며 index값을 매개변수로 받는다.
    fetcher,
);

6. Optimistic UI

채팅을 작성하다 보면 api에 요청 하고 결과를 돌려 받는데 약간의 딜레이가 발생한다.
그렇기 때문에 일단 UI에 반영을 먼저 하고, api요청하는 ui를 Optimistic UI라 한다.
안정성보다 사용성을 더 중요 할 경우 사용하는 기법이다.


* 기타 자바스크립트 문법

1. Optional chaining

프로퍼티가 없는 중첩 객체를 에러 없이 안전하게 접근할 수 있다.

2. Nullish coalescing

Nullish coalescing (??) 는 왼쪽 피연산자가 null 또는 undefined일 때 오른쪽 피연산자를 반환하고, 그렇지 않으면 왼쪽 피연산자를 반환하는 논리 연산자이다.

3. Object.entries

가끔 작업하다보면 객체를 기준으로 반복을 돌려야하는 경우가 있다. 나는 보통 Object.keys함수로 객체의 키값을 배열로 만들어 해당 배열 수 만큼 반복을 돌려 Object의 키값과 index value값을 비교해 반복을 돌렸었다.

4. flat

flat() 메서드는 모든 하위 배열 요소를 지정한 깊이까지 재귀적으로 이어붙인 새로운 배열을 생성

const arr1 = [1, 2, [3, 4]];
arr1.flat();
// [1, 2, 3, 4]
profile
고급개발자되기

0개의 댓글