[코드캠프]16일차_TIL_스프레드연산자

윤성해·2023년 4월 5일
0

프론트엔드_TIL

목록 보기
17/27
post-thumbnail

🏷️ 수업 목표

  1. 객체 / 배열 복사
  2. 댓글 수정하기
  3. 무한스크롤
  4. refactoring - 스프레드 연산자

1. 객체 / 배열 복사

자바스크립트에서 데이터를 복사할 때, 두가지 개념이 존재한다.

  1. 얕은 복사(Shallow Copy)
  2. 깊은 복사(Deep Copy)

복사

이런경우 aaa는 원본, bbb는 복사본이 된다.

let aaa = "철수"
let bbb = aaa

console.log(aaa) // 철수
console.log(bbb) // 철수

그리고 복사본의 값을 변경해서 재할당 해보자

bbb = "영희"

console.log(aaa) // 철수
console.log(bbb) // 영희

원본인 aaa의 값은 변하지 않고, 복사본인 bbb의 값만 변한 것을 확인할 수 있습니다.

객체 복사

let child1 = {
	name: "철수",
	age: 8,
	school: "다람쥐초등학교"
}
let child2 = child1

child2 // {name: '철수', age: 8, school: '다람쥐초등학교'}

그리고 복사본인 child2 객체의 name 값을 변경해봅시다.

child2.name = "영희"

child1 // {name: '영희', age: 8, school: '다람쥐초등학교'}
child2 // {name: '영희', age: 8, school: '다람쥐초등학교'}

복사본인 child2의 name값만 변경했는데, 왜 child의 name값도 변경되었을까? => 자바스크립트의 데이터 타입 특징 때문에 생기는 문제!!


String, Number, Boolean 타입 데이터의 경우 변수에 값을 할당하면 값 자체가 저장되지만, 객체와 배열의 경우, 그 값 자체가 아닌 주소가 저장됩니다. 두 객체가 같은 주소를 가지고있어서 child2의 값을 변경하면 child1의 값도 동일하게 변경되었던 것!

결론부터 말하자면 객체 복사라는 것은 존재하지 않는다.
원본 객체와 같은 값을 가진 객체를 새로 만들 수 있을 뿐!!!!!!
만들어봅시당

// child3에 child2 복사
let child3 = {
    name: child2.name,
    age: child2.age,
    school: child2.school
}

child3 // {name: '영희', age: 8, school: '다람쥐초등학교'}
// child3의 name 변경
child3.name = "훈이"

child3 // {name: '훈이', age: 8, school: '다람쥐초등학교'}
child2 // {name: '영희', age: 8, school: '다람쥐초등학교'}

위와 같이 child2 객체의 각 값을 꺼내서 child3의 각 key에 할당해준다. 그렇게 하면 결과적으로 child2를 child3에 복사한 것과 같은 모습이 된다. 이것이 엄밀히 복사는 아니지만, 이것을 우리는 객체복사 라고 칭한다!

스프레드연산자

객체를 복사할 때마다 원본객체의 모든 값을 따로따로 가져오는것은 번거로웠다. 객체에 들어가 있는 데이터의 양이 많아지면 사실 불가능임

그래서 스프레드 연산자를 사용하면 좋다.
마침표 세 개를 연속해서 찍어주면, 해당 객체 내의 모든 값을 개별 요소로 분리할 수 있다. 스프레드 연산자를 이용해 객체를 복사하면 복사본의 값을 변경해도 원본 값이 변경되지 않는다.

let child4 = {
	...child2
}

child4 // {name: '영희', age: 8, school: '다람쥐초등학교'}

중첩 객체 복사 with 스프레드 연산자

객체안에 객체가 있을 경우, 스프레드 연산자를 사용해도 제대로 복사가 되지 않습니다. 예를들어

 let profile1 = {
    name: "철수",
    age: 8,
    school: "공룡초등학교",
    hobby: {
        first: "수영",
        second: "프로그래밍"
    }
}

let profile2 = {
    ...profile1
}
profile1.name = "영희"

profile1 // {name: '영희', age: 8, school: '공룡초등학교', hobby: {…}}
profile2 // {name: '철수', age: 8, school: '공룡초등학교', hobby: {…}}

스프레드 연산자를 이용해 객체 복사를 수행한 뒤 profile1의 name 값을 변경해주었다. 그렇게 하면 profile1의 name은 변경되지만 profile2의 name은 변경되지 않는다. 이제 영희의 첫번째 취미를 변경해본다면???

profile1.hobby.first = "축구"


프로필1,2의 hobby 부분을 보면 복사가 제대로 되지 않았다는 것을 볼 수 있다. 이 문제는, hobby라는 key에 대한 값도 주소 값으로 들어가 있기 때문에 발생하는 문제!!! 이것처럼 스프레드 연산자를 이용한 복사를, 얕은복사 라고 한다. 얕은 복사는 depth 1의 깊이를 가진 데이터까지는 복사할 수 있지만, depth 2 이상의 깊이를 가진 데이터는 복사하지 못함!

스프레드 연산자는 얕은복사다.

깊은복사


어떻게하면 depth2 이상의 깊이를 가진 데이터를 복사할 수 있을까?
객체를 문자열의 형태로 바꾸고, 그 문자열을 다시 객체로 바꾸어 새로운 변수에 담아주면 된다. JSON.stringifyJSON.parse 라는 메소드를 이용하면 객체/배열을 문자열로, 그리고 문자열을 객체/배열로 바꾸어 줄 수 있습니다.

💡 JSON이란?
JavaScript Object Notation의 약자.
Javascript 객체 문법으로 구조화된 데이터를 표현하기 위한 문자 기반의 표준 포맷입니다.

// JSON.stringify
JSON.stringify(profile1)
// '{"name":"철수","age":8,"school":"공룡초등학교","hobby":{"first":"수영","second":"프로그래밍"}}'
// JSON.parse
JSON.parse(JSON.stringify(profile1))
// {name: '철수', age: 8, school: '공룡초등학교', hobby: {first: '수영', second: '프로그래밍'}}

배열 복사

배열도 객체와 같은 방식으로 복사가 가능하다.

const aaa = ["철수", "영희", "훈이"]
const bbb= [...aaa]

댓글 수정하기

객체 / 배열 복사에 대해 알아본 것들을 댓글 수정하기에 적용해보자.

댓글 목록은 다음과 같이 배열의 형태로 들어온다.
그리고 우리는 map을 이용해 댓글 데이터를 화면에 뿌려주고 있음!

이중 하나의 댓글을 선택해서 수정한다고 가정했을 때, 댓글의 수정하기 버튼을 누르면 해당 댓글 영역만 수정하기 input으로 바뀌어야 함 아래처럼 작성해보자

import { gql, useQuery } from "@apollo/client";
import { useState } from "react";

const FETCH_BOARDS = gql`
  query fetchBoards($page: Int) {
    fetchBoards(page: $page) {
      _id
      writer
      title
    }
  }
`;

export default function PaginationPage() {
  const { data } = useQuery(FETCH_BOARDS, { variables: { page: 1 } });

	const [myIndex, setMyIndex] = useState([
    false,
    false,
    false,
    false,
    false,
    false,
    false,
    false,
    false,
    false,
  ]);

	const onClickEdit = (event: MouseEvent<HTMLButtonElement>): void => {
	  const temp = [...myIndex];
	  temp[Number(event.currentTarget.id)] = true;
	  setMyIndex(temp);
	};

  return (
		<div>
      {data?.fetchBoards.map((el, index) =>
        !myIndex[index] ? (
          <div key={el._id}>
            <span>{el.title}</span>
            <span>{el.writer}</span>
            <button onClick={onClickEdit} id={String(index)}>
              수정하기
            </button>
          </div>
        ) : (
          <input type="text" key={el._id}></input>
        )
      )}
    </div>
  );
}

위처럼 10개가 모두 false로 하면 비효율적이니까 바꿔보자

import { useState } from "react";

export default function CommentItem(props: any): JSX.Element {
  const [isEdit, setIsEdit] = useState(false);

  const onClickEdit = (): void => {
    setIsEdit(true);
  };

  return (
    <div>
      {!isEdit ? (
        <div>
          <span style={{ margin: "10px" }}>{props.el.title}</span>
          <span style={{ margin: "10px" }}>{props.el.writer}</span>
          <button onClick={onClickEdit}>수정하기</button>
        </div>
      ) : (
        <input type="text" />
      )}
    </div>
  );
}

Item 컴포넌트로 분리하기(isEdit)

댓글의 수가 얼마나 될지 예상할 수 없는데, 그 개수 만큼의 길이를 가진 배열로 state를 관리하는 것은 비효율적입니다.

댓글 컴포넌트를 따로 분리한뒤 각각의 컴포넌트에서 isEdit state를 관리하는 방식으로 댓글 수정 기능을 구현해보자.

각 컴포넌트가 가지고 있는 isEdit은 이름만 같고 서로 독립적이기 때문에 별도로 관리할 수 있어 유지보수 하기 굉장히 좋아진다.

// 목록
import { useQuery, gql } from "@apollo/client";
import {
  IQuery,
  IQueryFetchBoardsArgs,
} from "../../../src/commons/types/generated/types";
import CommentItem from "../../../src/components/units/16-comment-item";

const FETCH_BOARDS = gql`
  query {
    fetchBoards {
      _id
      writer
      title
    }
  }
`;

export default function StaticRoutingMovedPage(): JSX.Element {
  const { data } = useQuery<Pick<IQuery, "fetchBoards">, IQueryFetchBoardsArgs>(
    FETCH_BOARDS
  );

  return (
    <div>
      {data?.fetchBoards.map((el) => (
        <CommentItem key={el._id} el={el} />
      ))}
    </div>
  );
}
// item 컴포넌트
import { useState } from "react";
import { IBoard } from "../../../commons/types/generated/types";

interface ICommentItemProps {
  el: IBoard;
}

export default function CommentItem(props: ICommentItemProps): JSX.Element {
  const [isEdit, setIsEdit] = useState(false);

  const onClickEdit = (): void => {
    setIsEdit(true);
  };

  return (
    <>
      {!isEdit ? (
        <div>
          <span>{props.el.title}</span>
          <span>{props.el.writer}</span>
          <button onClick={onClickEdit}>수정하기</button>
        </div>
      ) : (
        <input type="text"></input>
      )}
    </>
  );
}

무한스크롤

유튜브 또는 페이스북과 같이, 페이지를 아래로 스크롤 하다가 종단점에 도달하면 새로운 데이터가 계속해서 추가되는 방식의 페이지 처리 방법을 무한스크롤 방식이라고 합니다.

react infinite scroller 을 사용해보자요~~

1) 설치하기

npm : npm install react-infinite-scroller
yarn : yarn add react-infinite-scroller
💡 타입스크립트 오류가 발생하는 경우,
yarn add -dev @types/react-infinite-scroller 설치

2) 무한스크롤 구현하기

  1. 추가로 받아온 데이터가 있는지 확인하여
    (👆🏻 사진 속 if문 if (!fetchMoreResult?.fetchBoardComments))
    추가 데이터가 없으면 기존 댓글을,
    추가 데이터가 있다면 기존 댓글에 추가 데이터를 더해서 리턴해주기!
  1. 무한스크롤을 적용하고 싶은 영역을 **InfiniteScroll** 태그를 사용하여 감싸기 (게시글 목록임)
  1. 스크롤이 해당 영역의 하단 끝에 닿았을 때 실행되어야 할 기능을 함수로 만들어 loadMore 요소에 지정해주기. pageStart, hasMore 속성도 추가!
  1. Apollo-Client의 useQuery에서 제공하는 fetchMore 함수를 함께 사용하면 다음 page에 해당하는 데이터를 불러와 기존 데이터 뒤에 이어지도록 붙여 줄 수 있다.

🚨 onLoadMore 함수 최상위에 data가 없으면 실행되지 않도록 if문 추가하기

import { useQuery, gql } from "@apollo/client";
import type {
  IQuery,
  IQueryFetchBoardsArgs,
} from "../../../src/commons/types/generated/types";
import InfiniteScroll from "react-infinite-scroller";

const FETCH_BOARDS = gql`
  query fetchBoards($page: Int) {
    fetchBoards(page: $page) {
      _id
      writer
      title
      contents
    }
  }
`;

export default function StaticRoutingMovedPage(): JSX.Element {
  const { data, fetchMore } = useQuery<
    Pick<IQuery, "fetchBoards">,
    IQueryFetchBoardsArgs
  >(FETCH_BOARDS);

  const onLoadMore = (): void => {
    if (data === undefined) return;

    void fetchMore({
      variables: { page: Math.ceil((data?.fetchBoards.length ?? 10) / 10) + 1 },
      updateQuery: (prev, { fetchMoreResult }) => {
        if (fetchMoreResult.fetchBoards === undefined) {
          return {
            fetchBoards: [...prev.fetchBoards],
          };
        }

        return {
          fetchBoards: [...prev.fetchBoards, ...fetchMoreResult.fetchBoards],
        };
      },
    });
  };

  return (
    <div>
      <InfiniteScroll pageStart={0} loadMore={onLoadMore} hasMore={true}>
        {data?.fetchBoards.map((el) => (
          <div key={el._id}>
            <span style={{ margin: "10px" }}>{el.title}</span>
            <span style={{ margin: "10px" }}>{el.writer}</span>
          </div>
        )) ?? <div></div>}
      </InfiniteScroll>
    </div>
  );
}

스프레드연산자로 리팩토링 하기!

// 리팩토링 전
const [writer, setWriter] = useState("");
const [title, setTitle] = useState("");
const [contents, setContents] = useState("");

// 리팩토링 후
const [inputs, setInputs] = useState({ writer: "", title: "", contents: "" });
// 리팩토링 전
const onChangeWriter = (e: ChangeEvent<HTMLInputElement>): void => {
    setWriter(e.target.value);
  };
const onChangeTitle = (e: ChangeEvent<HTMLInputElement>): void => {
    setTitle(e.target.value);
  };
const onChangeContents = (e: ChangeEvent<HTMLInputElement>): void => {
    setContents(e.target.value);
  };

// 리팩토링 후
const onChangeInputs = (event: ChangeEvent<HTMLInputElement>): void => {
    setInputs((prev) => ({ ...prev, [event.target.id]: event.target.value }));
  };

🤷🏻‍♀️ 궁금한 것


❗️ 알게된 것


✨ 느낀 것

profile
Slow and steady wins the race.

0개의 댓글