프론트엔드 - 12

송현섭 ·2023년 4월 2일

프론트엔드

목록 보기
13/24
post-thumbnail

객체 / 배열 복사


let aaa = "철수"
let bbb = aaa

console.log(aaa) // 철수
console.log(bbb) // 철수
  • javascript 에서는 값의 복사가 가능
    복사한 값은 원본 값과 달라서 변경되어도 원본에는 영향 X
    다만 객체, 배열 같은 참조타입 복사의 경우 복사값이 바뀌면 원본도 똑같이 바뀜!


  • 객체 복사의 경우 방법에 따라 얕은 복사, 깊은 복사로 나누어지게 됨





객체 복사 - (얕은 복사)

  • 객체, 배열 같은 참조 타입의 경우 변수에 지정해 주면(복사를 하면) 그 값이 담기는 것이 아닌 값이 담겨있는 주소값이 변수에 담김

  • 따라서 복사를 하게 되면 주소값이 담기고, 이는 곧 주소를 공유하는 것이기 때문에 복사본의 값을 바꾸면 원본의 값에도 영향을 주어 값이 바뀌게 되는 것




[문제 해결] = spread 연산자 활용

  • let Obj2 = [...Obj] 같이 spread 연산자를 이용하면 객체 값을 복사 후 값이 바뀌어도 원본에는 영향을 주지않음 (배열도 똑같은 방식으로 복사 가능)
    *이 경우, Obj 라는 객체를 spread 를 이용해서 풀어낸 다음 다시 [ ] 로 감싼 것을 Obj2에 할당한 것!
    새로운 [ ]로 감쌌기 때문에 이 순간 원본과는 다른 주소를 가지게 됨!



    [BUT!] => spread 연산자에는 한계점이 있음!





spread 연산자의 한계점

 let profile1 = {
    name: "철수",
    age: 8,
    school: "공룡초등학교",
    hobby: {
        first: "수영",
        second: "프로그래밍"
    }
}
  • 위와 같이 객체 안의 객체가 있는 경우, spread를 이용해 복사해도 객체 안의 객체는 복사되지 않음
    *객체 안의 객체는 풀어내지 않았기 때문에 해당 객체의 주소값 자체를 복사하기 때문!

  • 얕은 복사는 depth1 까지만 복사가 되기 때문에 이 경우 깊은복사가 필요함!







객체 복사 - (깊은 복사)


  • JSON.stringify 로 객체를 문자열로 바꾼 다음, JSON.parse 로 다시 문자열을 객체로 바꿔주면 깊은 복사가 됨
    *원리
    -객체가 JSON에 의해 문자열로 바뀌는 순간 기존의 데이터 주소와의 연결관계는 완전히 끊어지며 이후 다시 객체로 변환되면서 완전히 새로운 주소값을 가지게 됨

    -즉, JSON.parse 시 객체의 모든 것이 새로운 주소값을 가지면서 원본과는 완전히 다른 새로운 객체가 생성된 것




+a) 기능 라이브러리 lodash

  • 객체를 복사할 때마다 깊은 복사를 따로 해주기는 번거롭고 느림

  • 복사 같은 관련 작업을 도와주는 기능 모음 라이브러리 lodash
    ex. _.cloneDeep(value) => 복사 기능








댓글 수정 기능과 컴포넌트 분리




구현할 기능


  • 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>
  );
}
    1. myIndex 라는 변수에 초기값으로 data.fetchBoards의 요소 수 만큼의 false가 있는 배열 지정

    1. map 으로 뿌리는 요소 중 버튼의 id 값을 index로 지정

    1. 버튼을 클릭하면 onClickedit 함수가 실행되고, event로 가져온 id 값(index)를 temp(원본 배열을 복사한 값)에서 해당 index로 조회(temp(index))하여 그 값을 true로 바꿈

    1. 바뀐 temp 배열을 setMyIndex( ) 로 myIndex에 재할당

    1. 변경된 myIndex 배열값을 아래 return 문에서 값이 true 면 기존의 요소를, 값이 false면 input 요소를 렌더링 하도록 조건부 렌더링 설정

    1. 결과적으로 선택한 요소만 조건부 렌더링에 의해 input창이 렌더링되게 됨






각 Item 들을 컴포넌트로 분리하기

  • 위 코드의 경우 Boards를 조회하는 것이라 10개의 false 가 담긴 배열을 setState를 통해 map 함수 내에서 조건부 렌더링으로 활용 가능

  • 하지만 comments(댓글) 을 조회한다면 그 list 수가 가변적이라 배열을 미리 지정하기 어렵고, 그 수가 많아지면 state 초기값으로 배열을 담아두는 것이 비효율적임
    *효율적인 방법 => comments(댓글) 을 컴포넌트로 분리하자!

1. 목록 컴포넌트

// 목록
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>
  );
}
  • 각각의 fetch로 조회될 목록들을 map 함수 안에 <CommentItem> 이란 컴포넌트로 지정

  • 이에 따라 data에 생성되는 목록들 수 만큼만 렌더링되어 화면에 표출 됨
    *이 때 el = {el} 로 data 배열 안의 각 객체정보를 props 형태로CommentItem에 전달






2. Item 컴포넌트

// 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>
      )}
    </>
  );
}
  • props로 넘긴 el 은 CommentItem 안에서 props.el 로 활용하여 화면에 표출

  • 각각의 CommentItem은 독립적인 isEdit을 가지게 되고, 조건부렌더링을 통해 각각의 개별적 요소들만 isEdit 값에 따라 기존의 요소 or input 요소를 렌더링하게 됨
    *렌더링 되는 결과는 똑같으나 컴포넌트를 분리하여 좀 더 효율적인 관리가 가능







무한 스크롤


유튜브 또는 페이스북과 같이, 페이지를 아래로 스크롤 하다가 종단점에 도달하면

새로운 데이터가 계속해서 추가되는 방식의 페이지 처리 방법을 무한스크롤 방식이라고 함




react-infinite-scroller 활용


  • react-infinite-scroller 라는 라이브러리를 활용해서 무한 스크롤 구현 가능






  • 우선적으로 추가적으로 데이터를 불러오기 위해 onloadMore 란 함수를 작성하고 해당 함수 안에 Query 에서 fetchMore(추가로 데이터를 더 fetch)를 뽑아내서 위와 같이 작성
    *[const {data, fetchMore} = useQuery(FetchBoard) 로 data 외에 fetchMore 도 뽑아올 수 있음!]

  • fetchMore 내 변수(variables)의 값으로 현재 fetch 된 글 수를 10으로 나눈 값에 1을 더한 값으로 설정
    *[원리]
    1. 해당 fetchBoard API는 한 번 fetch 시 열 개의 글들을 불러옴
    2. 즉 1페이지에 10개의 글을 불러오는 것이고, page 수가 늘어날 때마다(더 많이 fetch할 때마다) 10개 씩 글 목록 수가 증가
    3. fetchMore는 현재 글 수보다 더 많은 data를 불러오는 것이기에 그 다음 page의 글들을 불러와야 함(변수의 page 값이 다음 페이지로 지정되야 함)

  • if문을 추가하여 fetchMoreResult 가 없다면 (추가적으로 불러올 데이터가 없다면) fetchBoardComments 는 현재의 값으로 리턴하고, 그렇지 않다면 이전의 fetch된 Comments에 추가적으로 불러온 Comments들을 spread 연산자로 합한 결과를 현재 값으로 리턴





  • 설치한 infinite-scroll 라이브러리를 이용해 무한 스크롤로 생성되게 하고 싶은 요소들을 <InfiniteScroll> 태그로 감싸 줌
  • 태그 안의 속성에 필요한 속성과 값을 넣어 줌, 그리고 이전에 만든 함수 onLoadMore를 loadMore 라는 태그 내 속성의 값으로 넣어 줌






최종 코드


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>
  );
}
  • 마지막으로 onLoadMore 함수 내 최상단에 data === undefined 일 때 함수를 종료하도록 코드를 추가로 입력
    *data 가 undefined일 때 fetchMore가 실행되면 오류가 발생하기에 이를 방지하기 위함
profile
막 발걸음을 뗀 신입

0개의 댓글