2023.04.25 TIL

0

TIL

목록 보기
17/37
post-thumbnail

오늘의 나는 무엇을 잘했을까?

리액트 강의를 처음 들었는데 내가 잘 모르던 실전 패턴들을 기록하여 정리하면서 들었다. 그래서 사전 느낌으로 나중에 혹시 생각이 잘 안나더라도 찾아볼 수 있게 잘 정리하였고 각 패턴과 연관성이 적은 부분은 추상화하여 표현하였다. 나중에 기억이 안나서 볼 때 조금 더 가독성을 생각하여 정리하였다.

오늘의 나는 무엇을 배웠을까?

React

  • 부트스트래핑
    • create-react-app으로 리액트 프로젝트 생성
      • npm init react-app <폴더이름>
    • 개발 모드 실행
      • npm run start
  • 리액트 개발자 도구
    • 크롬 익스텐션인 react developer tools를 받고, 개발자 도구에서 새로 생긴 components탭을 이용하여 컴포넌트들의 상태나 prop등을 확인할 수 있다.
  • index.html, index.js파일
    • index.html: 웹 브라우저에서 맨 처음 여는 파일, body태그 안에 root id를 가진 div가 하나 있다.
    • index.js: 리액트 코드중 가장 먼저 실행되는 스크립트 파일, render메서드를 통해 우리가 만든 jsx태그들을 root id를 가지는 div안에 집어넣는 동작을 한다.
  • JSX
    • js와 html을 함께 사용하듯이 작성할 수 있는 자바스크립트의 확장 문법. camelCase attribute명을 쓰인다.
    • jsx로 html 태그를 만들고 싶다면 반드시 하나의 태그로 감싸줘야 한다. div<React.Fragment>태그로 전체를 감싸주면 된다. 또는 빈 태그 <></>도 사용 가능하다.
  • 컴포넌트
    • 함수형 컴포넌트는 함수 이름 첫 글자가 꼭 대문자여야하고, 꼭 jsx문법으로 만든 리액트 엘리먼트를 반환해야 한다.
    • 리액트 엘리먼트를 조금 자유롭게 다루기 위한 하나의 문법
  • 리액트 엘리먼트
    • JSX문법으로 작성한 요소는 자바스크립태 객체가 된다. 그리고 이런 객체들을 리액트 엘리먼트라고 부른다. index.js의 render함수에 이런 JSX로 만든 객체(리액트 엘리먼트)를 넘겨주면, 리액트가 이 객체 형태 값을 해석해서 html태그로 브라우저에 띄워주는 구조이다.
    • 바벨이라는 트랜스파일러를 사용하여 js코드로 변환되어 브라우저가 해석할 수 있게 된다. 바벨이 대표적인 트랜스파일러이다.
  • props
    • 컴포넌트에 속성을 지정하는 방법, 리액트 개발자 도구의 components 탭에서 컴포넌트를 클릭하면 해당 컴포넌트의 props를 확인할 수 있다.
    • children prop: props 객체에 기본적으로 들어있는 프롭값으로, 자식 노드들을 말한다. 다음과 같이 쓸 수 있다. children 프롭을 잘 사용하면 jsx문법을 html태그 작성하듯이 조금 더 직관적으로 사용할 수 있다.
      function Button(){
      	return <button>{props.children}</div>;
      }
      
      ...
      <Button>안녕 난 버튼이야</Button>
  • State
    • 리액트에서 재렌더링이 일어나기 위해 관리하는 변수
    • state가 배열이나 객체같은 참조형 변수면 setState로 내부 값을 변경했다 해도 참조값 자체는 변하지 않아 재 렌더링이 일어나지 않는다. 따라서 항상 이전 참조값을 복사하고 setState함수에는 복사본(스프레드 연산자 등으로)을 넘겨줘야 진짜 바뀐 것으로 인식하여 재렌더링이 일어난다.

React Component

컴포넌트는 웹 페이지를 구성하는 부품과도 같은 것이다. 부품 하나도 작은 부품으로 구성될 수 있듯이 컴포넌트도 더 작은 단위의 컴포넌트로 이루어질 수 있다.

  • 장점
    • 반복적인 개발이 줄어든다 (컴포넌트의 재사용성 덕분에)
    • 버그를 찾고 고치기 쉽다(컴포넌트 별로 모듈화가 잘 되므로)
    • 일을 쉽게 나눌 수 있다. (컴포넌트 단위로 각자 개발한 뒤 조립만 하면 되므로)

Virtual DOM

바닐라 자바스크립트로 컴포넌트를 변경해야 하는 경우, 변경이 필요한 모든 DOM요소들을 불러와 각 프로퍼티나 애트리뷰트를 직접 바꿔줘야 한다. 이런 번거로움과 불안정성을 제거하기 위해 리액트는 아예 새로 렌더링을 하는 방식을 택한다.

그런데 이렇게 하면 바뀌지 않는 부분도 다시 렌더링되어서 불필요한 성능 저하가 일어나지 않을까?

이런 것을 방지하기 위해 리액트는 Virtual DOM이란 것을 사용한다.

리액트가 엘리먼트를 렌더링할 때 바로 DOM트리에 반영하는 것이 아니라 Virtual DOM에 먼저 반영한다(이 때 이전 컴포넌트를 전부 지우고 통채로 새로 만들게 됨). 그리고 실제 DOM트리에 반영하기 전에 이전 Virtual DOM과 변화를 반영한 Virtual DOM을 비교하여(일종의 스냅샷 비교) 실제로 변경사항이 있는 부분만 실제 DOM에서 변경한다. 리액트는 이렇게 효율적인 화면변경과 렌더링을 위해 Virtual DOM이라는 자료구조를 사용한다.

React Tips(실습하면서 새로 알게 된 팁들)

  • 이미지 import: 다음과 같이 이미지를 임포트 하면 src안에 임포트한 이미지의 url이 들어간다. 이 때 rock.svg안에서는 export할 필요가 없다.

    // 여기에 코드를 작성하세요
    import Rock from "./assets/rock.svg";
    
    export default function HandIcon(){
      return <img src={Rock} alt="" />;
    }
  • 인라인 스타일에 backgroundImage속성

    다음과 같이 템플릿 리터럴로 url안에 import한 이미지를 넣어줘야 한다. 그리고 style attribute에는 하나의 객체가 들어간다. 즉, style={스타일}이 아닌 style={ {스타일} } 이 맞다.

     import HandIcon from './HandIcon';
      import purpleImg from './assets/purple.svg';
      
      function HandButton({ value, onClick }) {
        const handleClick = () => onClick(value);
        return (
          <button 
            onClick={handleClick}
            style={{
              backgroundImage: `url(${purpleImg})`,
              backgroundRepeat: "no-repeat"
            }}
          >
            <HandIcon value={value} />
          </button>
        );
      }
      
      export default HandButton;
  • 컴포넌트 자신의 스타일(버튼 색, 폰트 크기 등)은 컴포넌트 안에서, 버튼 끼리의 margin과 같은 외부에 영향을 미치는 스타일은 부모 요소에서 지정해주는 것이 맞다.

  • className 잘 사용하기: 다음과 같이 className prop에 문자열로 넣어주면 되고, 재사용성을 위해 className prop을 부모 컴포넌트에서 받으면 더 좋다.

    import diceImg from './assets/dice.png';
    import './Dice.css';
    
    function Dice({ className = '' }) {
      const classNames = `Dice ${className}`;
      return <img className={classNames} src={diceImg} alt="주사위 이미지" />;
    }
    
    export default Dice;

    더 프로답게 쓰는 방법은 템플릿 리터럴보다 배열을 사용하는 것이다.

    function Button({ isPending, color, size, invert, children }) {
      const classNames = [
        'Button',
        isPending ? 'pending' : '',
        color,
        size,
        invert ? 'invert' : '',
      ].join(' '); //배열의 join연산으로 문자열로 바꿈
      return <button className={classNames}>{children}</button>;
    }
    
    export default Button;

    혹은 다음과 같이 classnames라는 라이브러리를 사용하는 방법도 있다. (npm으로 설치)

    import classNames from 'classnames';
    
    function Button({ isPending, color, size, invert, children }) {
      return (
        <button
          className={classNames(
            'Button',
            isPending && 'pending',
            color,
            size,
            invert && 'invert',
          )}>
         { children }
       </button >
      );
    }
    
    export default Button;

React 배포하기

  • 빌드
    jsx문법은 브라우저가 해석하지 못하기 때문에 js코드로 변환해야하는데, 이 과정을 빌드라고 한다.
    • npm run build명령어로 빌드할 수 있다.
    • build가 끝나면 프로젝트 디렉토리에 build라는 폴더가 생긴다.
    • 그 폴더 내용 전체를 서버에 배포하면 된다.
    • 배포 전에 serve라는 npm패키지로 로컬 서버에서 build된 프로젝트를 테스트할 수 있다.
      • npx serve build
    • AWS S3로 배포하기
      • AWS관리 콘솔→스토리지→s3→버킷 만들기
      • 버킷은 컴퓨터의 c드라이브, d드라이브와 같이 s3에서 파일을 모아두는 큰 단위
      • 버킷 이름 정하기→고유해야함. 만약 커스텀 도메인을 적용할 거라면 그 도메인과 동일한 버킷이름을 써야함!
      • 퍼블릭 엑세스 차단 풀기(체크박스 해제)
      • 버킷 만들기 버튼 클릭
      • 만들어지면 버킷 이름 클릭해서 버킷으로 들어감
      • 버킷에서 속성→ 정적 웹사이트 호스팅→편집, 활성화
      • 인덱스 문서에 index.html, 오류문서에도 index.html (리액트 프로젝트라서 404는 index.html에서 처리 가능)
      • 버킷 정책 생성
        • policy type을 s3로 설정
        • 권한을 모두에게 부여(principle에 * 부여)
        • ARN란에 우리 버킷의 arn 입력(버킷 페이지에 있음)후 뒤에 “/*”를 입력(모든 파일에 정책을 적용하겠다는 뜻)
        • 정책 생성 누르고 나온 JSON문자열을 복붙(정책 란에)
      • 빌드 파일 업로드: 버킷의 객체 탭으로 가서 업로드 버튼을 누르고 빌드폴더에 있는 모든 파일들을 드래그 앤 드롭→업로드 클릭→닫기 클릭
      • 확인하기: 버킷메뉴의 속성탭 → 스크롤 내리면 정적 웹 사이트 호스팅 → 버킷사이트 엔드포인트 란 밑에 있는 url이 배포된 우리의 사이트이다.
    • 번들링
      • 빌드 시 여러 js파일들의 몇 개의 압축된 js파일들로 만들어진다. 이 과정을 번들링이라고 한다.

React로 데이터 다루기(실전 패턴 모음)

  • jsx는 for문을 사용할 수 없음으로 forEach나 map메서드를 활용하자.

  • 정렬 기능 구현

    예를 들어 여러 아이템을 별점 순으로, 또는 이름 순으로 정렬하는 필터를 구현해보자.

    아이템 리트스를 렌더링하는 부모 컴포넌트에서 order라는 스테이트를 만들어 이 순서를 관리할 수 있다. 필터 기준이 아이템의 프로퍼티가 된다. order에는 아이템의 프로퍼티가 들어간다. createdAt이 들어가 있다면 생성 날짜로 정렬되고, rating이 들어간다면 별점 기준으로 정렬되는 등 기능 구현을 할 수 있다.

    import items from '../mock.json';
      
      function App(){
      	const [order, setOrder] = useState('createdAt');
      	const sortedItems = items.sort((a, b)=> b[order]-a[order]);
      	
      
      	const handleNewestClik = ()=>setOrder('createdAt');
      	const handleBestClick = ()=>setOrder('rating');
      	return (
      		<div>
      			<button onClick={handleNewestClick}>최신순</button>
      			<button onClick={handleBestClick}>베스트 순</button>
      			<ReviewList items={sortedItems} />
      		</div>
      	);
      }
  • 리스트 아이템 삭제 기능 구현(filter)


    각 아이템에 삭제 버튼이 있다고 가정하고 filter메서드로 삭제기능을 구현해보자.

     import mockItems from '../mock.json';
     
     function App(){
     	const [items, setItems] = useState(mockItems);
     	
     	const handleDelete = (id) =>{
     		const newItems = items.filter((item)=>item.id !== id);	
     		setItems(newItems);
     	}
     	return (
     		<div>
     			
     			<ReviewList items={sortedItems} />
     		</div>
     	);
     }
     ...
     function ReviewListItem({item, onDelete}){
     	const handleDeleteClick = ()=>onDelete(item.id);
     
     	return (
     		...
     		<button onClick={handleDeleteClick}>삭제하기</button>
     		...
     	);
     }
     ...
     function ReviewList({items, onDelete}){
     	...
     	return (
     		items.map((item)=>{
     			return <ReviewListItem item={item} onDelete={onDelete} />
     		});
     	);
     }
  • map 메소드에서 key가 필요한 이유

    • 배열의 인덱스는 key로 사용할 수 없다. 삭제되고 추가될 때 인덱스가 바뀔 수 있기 때문이다.
    • key가 있어야 요소를 원하는 위치에 렌더링 할 수 있다. key가 바뀌면 엉뚱한 위치에 렌더링 되기도 해서 key는 절대 변하지 않는 고유의 값으로 설정해줘야 한다. 데이터의 id값은 괜찮은 key 후보이다.
    • key를 전달하지 않으면 배열이 변했을 때 어떤 방법, 어떤 순서로 변했는지 리액트가 구분을 하지 못한다. [사과, 포도, 망고] 배열이 [사과, 망고] 로 변했다면, 포도가 삭제된 건지, 망고가 삭제된 후 포도를 망고로 바꾼건지 구분할 수 없다. 하지만 key가 있다면 무조건 변화가 어떻게 일어난건지 알 수 있다.
    • map함수가 리턴하는 반복된 요소의 최상위에 key값을 prop으로 주면 된다.
  • fetch 함수 async await 할 때 사용 주의점

    • 꼭 await 두번 쓸 것.
       export const getFoodData = async () =>{
         const response  = await fetch("https://learn.codeit.kr/2266/foods");
         const { foods } = **await** response.json();
         return foods;
       }
  • useEffect에서 api콜로 받아온 데이터를 정렬하기

    • 데이터를 서버에서 받아온 후 수동으로 정렬해줄 수도 있지만 서버에서 정렬된 데이터를 받아올 수도 있다.

      	...
      	const handleLoad = async (order) => {
          const { foods } = await getFoods(order);
          setItems(foods);
        };
      
        const sortedItems = items.sort((a, b) => b[order] - a[order]);
      
        useEffect(() => {
          handleLoad(order);
        }, [order]);
      	...
      
      // api.js
      export async function getFoods(order = '') {
        const query = `order=${order}`;
        const response = await fetch(`https://learn.codeit.kr/api/foods?${query}`);
        const body = await response.json();
        return body;
      }
      
  • 페이지네이션 사용 패턴

    • 오프셋 기반

      • 오프셋: 지금까지 받아온 데이터의 개수

      • 오프셋 기반 페이지네이션은 서버의 데이터가 추가되거나 삭제되면 중복된 데이터를 불러오거나 데이터가 누락되는 등의 문제가 있음. 데이터 기준이 아닌 서버의 데이터 수를 기준으로 알려주기 때문이다.

    • 커서 기반

      • 커서: 지금까지 받은 데이터를 표시한 책갈피

      • 커서 기반은 서버에서 구현하기 까다롭기도 하고, 데이터의 변화가 많지 않을 때는 오프셋기반 api도 충분히 사용할 수 있다.

    • 오프셋 기반 페이지네이션 api 사용 패턴

       // api.js
       export async function getData({order="createdAt', offset = 0, limit = 6}) {
       	const query = `order=${order}&offset=${offset}&limit=${limit}`;
       	const response = await fetch(
       		`https://learn.../api/film-reviews?${query}`
       	);
       	const body = await response.json();
       	return body;
       }
       
       ...
       //app.js
       
       const LIMIT = 6;
       
       function App(){
       	..
       	const [offset, setOffset] = useState(0);
       	const [hasNext, setHasNext] = useState(true);
       
       	const handleLoad = async (options) =>{
       		const {reviews, paging} = await getData(options);
       		if (options.offset == 0)
       			setItems(reviews);
       		else setItems([...items, ...reviews]);
       		setOffset(options.offset + reviews.length);
       		setHasNext(paging.hasNext);
       	}
       	const handleLoadMore = async (options) =>{
       		handleLoad({order, offset, limit: LIMIT});
       	}
       	useEffect(()=>{
       		handleLoad({order, offset: 0, limit: LIMIT});
       	}, [order]);
       	...
       	return (
       		...
       		<button disabled={!hasNext} onClick={handleLoadMore}>더 보기</button>
       		...
       	);
       }
    • 커서 기반 페이지네이션 api 사용 패턴

      import { useEffect, useState } from 'react';
      import { getFoods } from '../api';
      import FoodList from './FoodList';
      
      const LIMIT = 10;
      
      function App() {
        const [order, setOrder] = useState('createdAt');
        const [items, setItems] = useState([]);
        const [cursor, setCursor] = useState('');
      
        const handleNewestClick = () => setOrder('createdAt');
      
        const handleCalorieClick = () => setOrder('calorie');
      
        const handleDelete = (id) => {
          const nextItems = items.filter((item) => item.id !== id);
          setItems(nextItems);
        };
      
        const handleLoad = async (options) => {
          const { foods, paging } = await getFoods(options);
          if(!options.cursor){
            setItems(foods);
          }
          else {
            setItems((prev) => [...prev, ...foods]);
          }
          console.log(paging.nextCursor);
          setCursor(paging.nextCursor);
        };
      
        const handleLoadMore = () => {
          handleLoad({order, cursor, limit: LIMIT});
        };
      
        const sortedItems = items.sort((a, b) => b[order] - a[order]);
      
        useEffect(() => {
          handleLoad({order, cursor, limit: LIMIT});
        }, [order]);
      // https://learn.codeit.kr/api/foods?order=createdAt&limit=10
        return (
          <div>
            <button onClick={handleNewestClick}>최신순</button>
            <button onClick={handleCalorieClick}>칼로리순</button>
            <FoodList items={sortedItems} onDelete={handleDelete} />
            <button disabled={!cursor} onClick={handleLoadMore}>더보기</button>
          </div>
        );
      }
      
      export default App;
      
      ...
      //api.js
      export async function getFoods({ order = '', cursor = '', limit = 10 }) {
        const query = `order=${order}&cursor=${cursor}&limit=${limit}`;
        const response = await fetch(`https://learn.codeit.kr/api/foods?${query}`);
        const body = await response.json();
        return body;
      }
  • 비동기로 State변경 시 주의점

    • 비동기 함수 안에서의 스테이트값은 비동기 처리되기 전의 스테이트값이다. 즉, 비동기를 실행하고 난 뒤 처리되기 전까지 어떤 활동이 일어나 스테이트의 값이 변경되었다 해도 비동기 함수 안에서는 여전히 그 이전의 스테이트 값이 저장되어 있다.
      const handleDelete = (id) =>{
      	const nextItems = items.filter((item)=>item.id !== id);
      	setItems(nextItems);
      }
      
      const handleLoad = async (options) =>{
      	const {reviews} = await getReviews();
      	setItems([...items, ...reviews]); //getReviews가 처리되기 전에 handleDelete가 불리면
      	//컴포넌트의 items 스테이트는 하나가 삭제된 것으로 변경이 되었지만 
      	// 여기 이 handleLoad는 비동기 함수라서 여기서 사용하는 items는 handleLoad함수가
      	// 호출되는 시점의 items 값을 가지고 있음.
      }
      
      // 따라서 다음과 같은 setState형태로 만들어주면 된다.
      setItems((prev)=>[...prev, ...reviews]);
      // 이전 state값을 사용하겠다고 명시한 코드
  • loading 스테이트 사용 패턴

    • 비동기 리퀘스트를 보내는 경우 loading중 상태를 애니메이션 등으로 표시하고 싶을 때 사용하는 패턴이다.
      function Component(){
      	const [isLoading, setIsLoading] = useState(false);
      	
      	const handleLoad = async (options) =>{
      		let result;
      		try {
      			setIsLoading(true); //api호출 직전에 로딩값을 true로 설정
      			result = await apiCall();
      		} catch(err){
      			
      		} finally {
      			setIsLoading(false);  //에러가 나든 성공하든 끝나면 로딩값을 false로
      		}
      	}	
      }
  • 네트워크 에러 처리 패턴

    • 네트워크 에러가 발생했을 때 페이지에 오류를 잘 보여주기 위한 패턴이다.
      function Component(){
      	const [loadingError, setLoadingError] = useState(null);
      
      	const handleLoad = async (options) =>{
      		let result;
      		try {
      			setLoadingError(null);
      			result = await apiCall();
      		} catch(err) {
      			setLoadingError(err);
      		}
      	}
      
      	return (
      		...
      		{loadingError?.message && <span>{loadingError.message}</span>}
      		...
      	);
      }

오늘의 나는 어떤 어려움이 있었을까?

생각보다 많은 것들을 새로 알게 되어서 정리하는 데 오래 걸렸던 것 같다. 그리고 양이 워낙 많아서 알고리즘 문제를 풀지 못하였다.

내일의 나는 무엇을 해야할까?

  • 알고리즘 문제 풀기
  • 위클리 미션 시작하기
  • 이번주 분량 리액트 강의 완강하기

0개의 댓글