2023. 3. 31

Junghan Lee·2023년 3월 31일
0

TIL Diary

목록 보기
29/52

index

객체&배열 복사(객체 복사, 스프레드 연산자, 깊은 복사, lodash, 배열 복사)
무한스크롤

객체, 배열 복사

-객체와 배열은 조금 더 넉넉한 공간이 필요하다-
const profile2 = {...profile} 얕복
const profile3 = JSON.parse(JSON.stringify(profile)) 깊복

복사? 얕은 복사(Shallow Copy) & 깊은 복사(Deep Copy)

복사의 기본 개념

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의 값만 변경했는데 child1의 값은 왜 변경되었을까? 데이터 타입의 특징 때문


String, Number, Boolean 타입 데이터 : 변수에 값을 할당하면 값 자체가 저장되는 반면
객체와 배열의 경우 값 자체가 아닌 '주소'가 저장된다.

객체를 복사한 뒤 값을 변경할 때, 복사는 문제없이 잘 이루어진 것이었다. child1에 할당된 객체의 주소 값이 child2에도 동일하게 들어가게 된 것

그러나 두 객체가 같은 주소를 가지고 있어서 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에 복사한 것과 같은 모습으로 엄밀히 말해 복사는 아니지만 이것이 객체 복사

객체 복사 수행 뒤, 복사본인 child3의 값을 변경해도 child2의 name값은 변경되지 않고 유지된다.

객체를 복사할 때마다 원본 객체의 모든 값을 따로따로 가져오기 번거롭기 때문에...

스프레드 연산자

마침표 세개를 연속해 찍어주면, 해당 객체 내의 모든 값을 개별 요소로 분리할 수 있다.

스프레드 연산자를 이용해 객체를 복사하게 되면
복사본의 값을 변경해도 원본 값이 변경되지 않는다.

let child4 = {
	...child2
}

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

스프레드 연산자를 통한 중첩 객체 복사
그러나 스프레드 연산자를 통한 복사도 만능은 아니다.
객체 안에 객체가 값으로 들어가 있는 경우 제대로 복사되지 않는다.

 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값을 변경해도 profile2의 name값이 변하지 않는다.

그러나 hobby를 변경했을 때는

복사가 제대로 되지 않았다는 것을 알 수 있다.

hobby라는 key에 대한 값도 주소값으로 들어가있기 때문에 이런 문제가 발생한다. 이처럼 스프레드 연산자를 이용한 복사를 얕은 복사라고 한다.

얕은 복사는 depth1의 깊이를 가진 데이터까지는 복사할 수 있으나 그 이상의 깊이를 가진 데이터를 복사하지 못한다.

depth2 이상의 데이터를 복사하기 위해서는

깊은 복사

를 해야 한다.

객체를 문자열의 형태로 바꾸고 그 문자열을 다시 객체로 바꾸어 새로운 변수에 담는 것이다. JSON.stringify, JSON.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: '프로그래밍'}}

lodash

객체를 복사할 때마다 깊은 복사를 따로 해주는 것은 번고롭고 느릴 수 있다. 그래서 관련 작업을 도와주는 라이브러리를 이용하는 방법이 있다.

그중 다수가 사용하는 lodash가 있다.

https://lodash.com/docs/4.17.15
lodash에서 제공하는 _.cloneDeep(value)를 사용하면 깊은 복사를 할 수 있다.
참고로 lodash는 이외에도 다양한 기능을 제공한다.

배열 복사

그렇다면 객체가 아닌 배열은 어떻게 복사해야할까?
객체와 같은 방식으로 복사가 가능하다.

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>
  );
}

Item 컴포넌트로 분리
댓글의 수가 얼마나 될지도 모르는데 그 개수만큼의 길이를 가진 배열로 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라는 라이브러리를 통해 이를 쉽게 구현할 수 있다.(react infinite scroll compoonent 또한 많이 사용된다.)

yarn add -dev @types/react-infinite-scroller

를 터미널에 입력해 타입을 설치할 수 있고

yarn add react-infinite-scroller

이것이 기본적이 설치 명령어다.

  const 이전댓글들 = [댓글1~댓글30 //prev
  const 추가댓글들 = [댓글 31, ~ ,댓글50] //fetchMoreResult
  const 전체댓글 = [...이전댓글들, ...추가댓글들] //댓글1~50


추가로 받아온 데이터가 있는지 확인해
없으면 기존 댓글을, 있으면 기존 댓글에 추가 데이터를 더해 리턴한다.

fetchBoards에 무한 스크롤 적용시

스크롤이 해당 영역의 하단 끝에 닿았을 때 실행되어야 할 기능을 함수로 만들어 loadMore 요소에 지정하면 된다. pageStart, hasMore속성도 추가한다.

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 }) => {
  	// !fetchMoreResult.fetchBoards
        if (fetchMoreResult.fetchBoards === undefined) {
          return {
            fetchBoards: [...prev.fetchBoards],
          }; // more 없으면 기존의 것만 페치
        }

        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 }));
  };

기존에 항목별로 따로 선언했던 state를 하나의 state로 묶어 객체 형태로 만든다.

객체 key에 대괄호를 씌우면 자바스크립트 변수가 된다.

profile
Strive for greatness

0개의 댓글