본문 바로가기

Frontend Study

[유데미x스나이퍼팩토리] Next.js 3기 - 사전직무교육 4일차

재사용성이 높은 컴포넌트 만들기

props로 tailwind의 className과 HTML 속성들을 전달해 쉽게 스타일 및 속성을 변경 할 수 있음

ComponentPropsWithoutRef → React에서 특정 HTML 요소나 React 컴포넌트에 대한 props 타입을 유추할 때 사용하는 유틸리티 타입

쉽게 말해, 태그 내에 쓰이는 props들의 타입들을 자동으로 추론해서 전달해 줌

전달받은 props들은 구조 분해 할당을 통해 필요한 것들만 사용

//App.tsx
export default function App() {
  return (
    <Input className="bg-black" type="text" placeholder="입력" />
  );
}

//Input.tsx
import { twMerge } from "tailwind-merge";

type InputProps = React.ComponentPropsWithoutRef<"input">;

export const Input = (props: InputProps) => {
  //구조 분해 할당으로 className과 기타 props들을 구분
  const { className, ...rest } = props;
  //className으로 스타일 추가 가능
  return (
    <input
      className={twMerge("w-20 h-5", className)}
      // type, value, placeholder 등 다양한 rest로 props들을 전달
      {...rest}
    ></input>
  );
};

조건부 렌더링

  1. if문 사용
export default function App() {
  const isTrue = true;
  if (isTrue) return <h1>true</h1>;
  return <h1>false</h1>;
}

  1. 삼항연산자 사용
export default function App() {
  const isTrue = true;
  return isTrue ? <h1>true</h1> : <h1>false</h1>;
}

  1. 조건이 참일때만 렌더링 (&&)
export default function App() {
  const isTrue = true;
  return isTrue && <h1>true</h1>;
}

반복 렌더링

map() 메서드를 사용하여 배열의 내용을 반복

→ 고유한 key를 설정해 주어야 함(습관화 하기)

export default function App() {
  const array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
  return (
    <div>
      {array.map((num, idx) => (
        <h1 key={idx}>{num}</h1>
      ))}
    </div>
  );
}
// 1 2 3 4 5 6 7 8 9 10

객체 배열 렌더링 → 배열 안에 객체가 들어 있어야만 반복 가능

export default function App() {
  const array = [
    {
      num: 1,
      name: 'james'
    },
    {
      num: 2,
      name: 'kirk'
    },
    {
      num: 3,
      name: 'robert'
    },
    {
      num: 4,
      name: 'lars'
    },
  ]
  return (
    <div>
      {array.map((item) => (
        <h1 key={item.num}>{item.name}</h1>
      ))}
    </div>
  );
}

컴포넌트 역시 반복 가능

const Print = ({ num }: { num: number }) => {
  return <h1>{num}번째 컴포넌트</h1>;
};

export default function App() {
  const array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
  return (
    <div>
      {array.map((num, idx) => (
        <Print key={idx} num={num}></Print>
      ))}
    </div>
  );
}

이미지 불러오기

  1. public 폴더 안에 넣기
function App() {
  return (
    <div>
	    // 현재 주소/이미지 이름으로 접근
      <img src="<http://localhost:5173/cat.jpg>"></img>
    </div>
  );
}
export default App;

public 폴더는 빌드 전/후의 주소가 똑같음

  1. src 폴더 안에 넣기
// 이미지 파일의 경로에서 직접 import 해야 함
import bird from "./asset/bird.webp";

function App() {
  return (
    <div>
      <img src={bird}></img>
    </div>
  );
}
export default App;

src 폴더 안에 넣으면 빌드시 url이 달라짐

public 폴더에 넣으면 빌드가 되지 않음 → 빌드가 필요한 이미지는 src에 넣는게 좋음

  • 파비콘, 로고 등 → public 폴더
  • 컴포넌트 배경, 이미지 등 → src 폴더

gap을 이용한 스타일링

gap → 컴포넌트 내부 chilren 요소들이 일정 간격씩 떨어져 있도록 만드는 방법

<div className="flex flex-col gap-[10px]">
	<h1>1</h1>
	<h1>2</h1>
	<h1>3</h1>
</div>

//각 h1 태그가 10px씩 떨어져 있게 됨

React Hook

  1. useState

state → 상태 ⇒ React 내에서 사용하는 변수

import { useState } from "react";

function App() {
  const [num, setNum] = useState(0);
  return (
    <div>
      <h1>{num}</h1>
      <button onClick={() => setNum(num + 1)}>증가</button>
    </div>
  );
}
export default App;

useState는 비동기적이기 때문에 set을 여러개 써도 동작은 한번만 실행된다

import { useState } from "react";

function App() {
  const [num, setNum] = useState(0);
  const increase = () => {
    setNum(num + 1);
    setNum(num + 1);
		setNum(num + 1);
		//한번 실행했을 때, num = 1이 됨
  };
  return (
    <div>
      <h1>{num}</h1>
      <button onClick={increase}>증가</button>
    </div>
  );
}
export default App;

set 함수에는 매개변수로 콜백 함수를 넘길 수 있음 → 콜백함수는 동기적으로 실행되기 때문에 늘 최신 state를 이용

  • set 함수를 콜백 형식으로 만들어주지 않으면 render 시점의 값을 참조 => 빈 배열
  • 콜백 형식으로 만들어주면 이전의 값을 참조하기 때문에, 함수 실행 시점의 state(useState로 설정된 state)를 참조 => 값이 존재함
import { useState } from "react";

function App() {
  const [num, setNum] = useState(0);
  const increase = () => {
    setNum((num) => num + 1);
    setNum((num) => num + 1);
    setNum((num) => num + 1);
    //num이 1 증가 -> 증가한 num을 콜백함수의 매개변수로 전달
    //최종적으로 num은 총 3 증가
  };
  return (
    <div>
      <h1>{num}</h1>
      <button onClick={increase}>증가</button>
    </div>
  );
}
export default App;

상태값 참조 여부에 따라 매개변수로 상태를 전달할지, 콜백을 전달할지 정함

  • 원래 상태값을 참조해야 할 때 → 콜백함수
  • 참조하지 않아도 될 때 → 상태

👏 input 태그에서 required를 사용하면 자동으로 valid check를 해 줌

Props Drilling

useState만을 이용해서 다른 컴포넌트에 state를 전달하기 위해서는 자식 컴포넌트로 계속해서 state를 전달해야 하는 불편함이 생김 → Props Drilling이라고 함

  1. useRef

HTML Dom에 접근하거나, 렌더링 여부와 상관없이 값을 유지하고 싶을 때 사용

→ document selector(getElementById 등)

⇒ DOM 참조시 current 객체로 참조

import { useRef } from "react";

const App = () => {
  const inputEl = useRef<HTMLInputElement>(null);
  const onButtonClick = () => {
	  // current 객체를 통해 기능 접근 가능
    inputEl.current?.focus();
  };
  return (
    <div>
	    // inputEl이라는 ref로 input 태그를 참조
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </div>
  );
};

export default App;

부모 컴포넌트에서 자식 컴포넌트의 ref에 접근하기 위해서는 forwardRef() 메서드 사용

import { forwardRef } from "react";

export const Forward = forwardRef((ref) => {
  return <h1 ref={ref}></h1>;
});
export default Forward;

  1. useEffect
    • 컴포넌스 생성 : 웹 브라우저에 렌더링 되는 순간
    • 컴포넌트 삭제 : 컴포넌트 렌더링이 종료되는 순간
    • 컴포넌트 수정 : 컴포넌트의 상태나 변수가 변경되었을 때
    useEffect → 생명주기 체크 가능
    1. 컴포넌트 생성
    import { useEffect } from "react";
    
    export default function App() {
      useEffect(() => {
    	  // 컴포넌트 생성(렌더링) 시점에 log 출력
        console.log("컴포넌트 생성");
      }, []);
      return <h1 className="text-3xl font-bold underline">Hello world!</h1>;
    }
    
    1. 컴포넌트 수정
    import { useEffect, useState } from "react";
    
    export default function App() {
      const [num, setNum] = useState(0);
      useEffect(() => {
        console.log("컴포넌트 생성");
      }, []);
    
      useEffect(() => {
        console.log("컴포넌트 수정");
      }, [num]);
      return (
        <>
          <h1 className="text-3xl font-bold underline">{num}</h1>
          <button onClick={() => setNum(num + 1)}>증가</button>
        </>
      );
    }
    
    1. 컴포넌트 삭제
      • 컴포넌트는 useEffect에서 함수를 반환 할 때 unmount 됨
    import { useEffect } from "react";
    
    const Interval = () => {
      useEffect(() => {
        const interval = setInterval(() => {
          console.log("Interval Component Updated!");
        }, 1000);
    
        return () => {
          clearInterval(interval);
        };
      }, []);
    
      return (
        <>
          <h1>Interval Component</h1>
        </>
      );
    };
    export default Interval;
    
    1. useLayoutEffect
    • useEffect는 컴포넌트가 화면에 그려진 후(paint)에 실행되는 hook
    • useLayoutEffect는 컴포넌트가 화면에 그려지기 전에 실행되는 hook
      • 내부 로직이 모두 실행된 후 화면에 그리기 때문에, 너무 복잡한 로직은 넣지 않는 것이 좋다
    import { useEffect, useLayoutEffect, useState } from "react";
    
    const UseLayoutEffect = () => {
      const [count, setCount] = useState(0);
      const now = performance.now();
      while (performance.now() - now < 200) {
        // Artificial delay -- do nothing
      }
      useLayoutEffect(() => {
        if (count === 10) setCount(0);
        console.log("useLayoutEffect");
      }, [count]);
      return (
        <>
          <h1>Count: {count} </h1>
          <button onClick={() => setCount(10)}>클릭</button>
        </>
      );
    };
    export default UseLayoutEffect;
    
  2. 컴포넌트 생명주기 → 컴포넌트 생성부터 삭제까지의 일련의 과정
    • 컴포넌스 생성 : 웹 브라우저에 렌더링 되는 순간
    • 컴포넌트 삭제 : 컴포넌트 렌더링이 종료되는 순간
    • 컴포넌트 수정 : 컴포넌트의 상태나 변수가 변경되었을 때
    useEffect → 생명주기 체크 가능
    1. 컴포넌트 생성
    import { useEffect } from "react";
    
    export default function App() {
      useEffect(() => {
    	  // 컴포넌트 생성(렌더링) 시점에 log 출력
        console.log("컴포넌트 생성");
      }, []);
      return <h1 className="text-3xl font-bold underline">Hello world!</h1>;
    }
    
    1. 컴포넌트 수정
    import { useEffect, useState } from "react";
    
    export default function App() {
      const [num, setNum] = useState(0);
      useEffect(() => {
        console.log("컴포넌트 생성");
      }, []);
    
      useEffect(() => {
        console.log("컴포넌트 수정");
      }, [num]);
      return (
        <>
          <h1 className="text-3xl font-bold underline">{num}</h1>
          <button onClick={() => setNum(num + 1)}>증가</button>
        </>
      );
    }
    
    1. 컴포넌트 삭제
      • 컴포넌트는 useEffect에서 함수를 반환 할 때 unmount 됨
    import { useEffect } from "react";
    
    const Interval = () => {
      useEffect(() => {
        const interval = setInterval(() => {
          console.log("Interval Component Updated!");
        }, 1000);
    
        return () => {
          clearInterval(interval);
        };
      }, []);
    
      return (
        <>
          <h1>Interval Component</h1>
        </>
      );
    };
    export default Interval;
    
    1. useLayoutEffect
    • useEffect는 컴포넌트가 화면에 그려진 후(paint)에 실행되는 hook
    • useLayoutEffect는 컴포넌트가 화면에 그려지기 전에 실행되는 hook
      • 내부 로직이 모두 실행된 후 화면에 그리기 때문에, 너무 복잡한 로직은 넣지 않는 것이 좋다
    import { useEffect, useLayoutEffect, useState } from "react";
    
    const UseLayoutEffect = () => {
      const [count, setCount] = useState(0);
      const now = performance.now();
      while (performance.now() - now < 200) {
        // Artificial delay -- do nothing
      }
      useLayoutEffect(() => {
        if (count === 10) setCount(0);
        console.log("useLayoutEffect");
      }, [count]);
      return (
        <>
          <h1>Count: {count} </h1>
          <button onClick={() => setCount(10)}>클릭</button>
        </>
      );
    };
    export default UseLayoutEffect;
    

본 게시글의 예제 코드는 수코딩(https://www.sucoding.kr)을 참고했습니다