Myalog/Study

[유데미x스나이퍼팩토리] 프로젝트 캠프 : React 2기 - 사전직무교육 Day 5

myalog 2024. 8. 25. 20:37

 

사전직무교육 Day5

 

 

온라인(Zep) 환경에서의 사전직무교육

 

오늘은 사전직무교육 다섯째 날이자 첫 주를 마무리 하는 주이다!

어제보다는 컨디션이 나아져서 온라인으로 교육을 들었다.

벌써 한 주가 흘렀다니 시간이 빠른 것 같다..🥹

 


 

⚙️ React Hooks

 

  • React Hooks는 16.8 버전에 추가됨
  • 리액트 훅이 추가됨으로써 함수형 컴포넌트의 컴포넌트 생명주기가 지원되기 시작
    • useState
    • useEffect
    • useContext
    • useReducer
    • useRef

useState

  • 추적 가능한 변수를 만들 때 사용
    • javaScript 변수는 let / const / var 를 사용
    • React 변수는 useState 훅을 통해 사용
  • 불변성의 원칙
    • 값이 변하지 않는 특징
    • 리액트에서는 불변성을 지켜서 값을 변경해야 함
      • 리액트에서 값을 바꿀 때, 불변성을 지켜줘라 === 리액트에서 값을 바꿀 때, 새로운 값을 만들어서 변경해라
      • setter 함수를 사용하면 불변성 유지가 가능
      • 값의 변경은 비동기로 업데이트 됨
        • 값의 변경을 동기적으로 업데이트 되게 하려면 콜백 함수를 받아와서 값을 반환
        • 이전의 최신 상태 값을 참조해야 할 경우 사용
        • 매개변수가 현재(최신) state 값을 보장해줌
          • setNum(num + 1) --> 동기
          • setNum((num) => num + 1) --> 비동기
        • 불변성을 유지해주는 메서드
          • slice
          • filter
          • map
          • reduce
          • concat
          • Object.assign
          • [...prev]
          • [...{ name: "Mia" }]

 

state를 한번에 관리하는 방법

  • 타입이 다른 것은 타입 오퍼레이터로 타입을 추가
  • state를 객체로 정리
    • 객체 병합 이용
      • 똑같은 속성이 있으면, 새로운 속성이 덮어씌워짐
      • 배열 병합은 뒤에 추가되어 병합됨
const [formState, setFormState] = useState({
	email: "",
	password: "",
	name: "",
})

const onChangeFormState = (e: React.ChangeEvent<HTMLInputElement>) => {
	setFormState((formState) => ({
		...formState,
		[e.target.name]: e.target.value
	}))
}
    <>
      <input
        type="email"
        value={formState.email}
        name="email"
        onChange={onChangeFormState}
      />
      <input
        type="password"
        value={formState.password}
        name="password"
        onChange={onChangeFormState}
      />
      <input
        type="text"
        value={formState.name}
        name="name"
        onChange={onChangeFormState}
      />
    </>

 

  • Custom Hook으로 만들기
// useInput.ts

import { useState } from "react";

type UseInputReturn = [
  string,
  (e: React.ChangeEvent<HTMLInputElement>) => void
];

function useInput(initialValue: string): UseInputReturn {
  const [value, setValue] = useState(initialValue);

  const onChangeValue = (e: React.ChangeEvent<HTMLInputElement>) => {
    setValue(e.target.value);
  };
  return [value, onChangeValue];
}

export default useInput;
import useInput from "./hooks/useInput";

const App = () => {
  const [email, onChangeEmail] = useInput("");
  const [password, onChangePassword] = useInput("");
  const [name, onChangeName] = useInput("");
  const [date, onChangeDate] = useInput("");

  return (
    <>
      <pre> {JSON.stringify({ email, password, name, date }, null, 2)}</pre>

      <input type="email" value={email} onChange={onChangeEmail} />
      <input type="password" value={password} onChange={onChangePassword} />
      <input type="text" value={name} onChange={onChangeName} />
      <input type="date" value={date} onChange={onChangeDate} />
    </>
  );
};
export default App;

 

useRef

  • useRef는 HTML 요소를 참조하는 용도로 사용할 수 있음
    • javaScript에서 document.querySelector() 와 같은 기능
  • useRef로 만들어진 변수를 참조하려면 변수명.current로 참조해야함
  • 코드적으로는 실행되고 있으나, 리액트에서 관리하는 코드가 아니라 화면에 실시간으로 업데이트가 되지 않음
  • 즉, useRef는 렌더링을 유발하는 훅이 아님
  • 따라서 useState 보다 성능에 유리한 상황이 있을 수 있는데, 코드 내부적으로만 업데이트 시키고 싶다면 useRef 활용하기
  • 제어하고 싶은 요소 당 한개씩 ref 로 만들어야 함
  • ref 가 한개 이상 발생 시 커스텀 훅으로 따로 만들기!
// 같은 컴포넌트 내에 참조할 요소가 있을 경우

const els = useRef<HTMLInputElement>(null);

<input ref={els} />
// 하위 컴포넌트에 참조할 요소가 있을 경우 (props로)
// forwardRef() 함수 사용

type TInputProps = Omit<React.ComponentPropsWithRef<"input">, "type"> & {
  type: "text" | "password" | "email" | "number" | "date";
};
const Input = forwardRef<HTMLInputElement, TInputProps>((props: TInputProps, ref) => {
  const { ...rest } = props;
  return (
    <>
      <input
	      ref={ref}
        {...rest}
      />
    </>
  );
});

Input.displayName = "Input";
export default Input;
  • forwardRef() 함수로 감쌀 경우 빌드 단계(npm run build)에서 에러가 날 수 있기 때문에, 컴포넌트명.displayName = "컴포넌트명"; 을 추가해야함

 

📚 오늘의 과제
  • 여행 사이트 구현하기
  1. Seoul, London, Paris, NewYork 총 4개의 나라 이름의 탭과 4개의 나라별 사진 리소스로 구현
  2. Seoul 탭을 클릭할 경우 서울 이미지만 나타나고, London 탭을 클릭할 경우 런던 이미지만 나타나도록 구현 (다른 탭 동일)
  3. 각 탭이 활성화 되었을 때 나라 이름이 bold 처리 되도록 스타일링
  4. 기본값은 Seoul 탭을 활성화
import travel from './assets/travel.png';
import seoul from './assets/seoul.jpg';
import london from './assets/london.jpg';
import paris from './assets/paris.jpg';
import newyork from './assets/newyork.jpg';
import { useState } from 'react';
export default function App() {
  const [currentTab, setCurrentTab] = useState(1);

  const countryList = [
    { id: 1, link: '#', countryName: 'Seoul' },
    { id: 2, link: '#', countryName: 'London' },
    { id: 3, link: '#', countryName: 'Paris' },
    { id: 4, link: '#', countryName: 'NewYork' },
  ];

  const imgList = [
    { id: 1, imgUrl: seoul, alt: '서울' },
    { id: 2, imgUrl: london, alt: '런던' },
    { id: 3, imgUrl: paris, alt: '파리' },
    { id: 4, imgUrl: newyork, alt: '뉴욕' },
  ];

  const onClickHandler = (index: number) => {
    setCurrentTab(index + 1);
  };
  return (
    <>
      <div>
        <div></div>
        <div></div>
      </div>
      <div>
        <div>
          <img src={travel} alt={'로고'} width={80} />
          <ul>
            {countryList.map((country, index) => (
              <li key={country.id}>
                <a
                  href={country.link}
                  onClick={() => onClickHandler(index)}
                  className={
                    index + 1 === currentTab ? 'font-bold' : 'font-medium'
                  }
                >
                  {country.countryName}
                </a>
              </li>
            ))}
          </ul>
          <div>
            {imgList.map((image) => (
              <div key={image.id}>
                {image.id === currentTab && (
                  <img src={image.imgUrl} alt={image.alt} />
                )}
              </div>
            ))}
          </div>
        </div>
      </div>
    </>
  );
}

 

Seoul, London, Paris, NewYork을 각각 1, 2, 3, 4번의 탭으로 구성했다.

나라 별 탭과 이미지를 map 메서드를 통해 반복 렌더링을 시키기 위해 실제 화면에 보여지게 될 각 요소를 객체에 담아 배열로 선언했다.

선택된 탭의 index를 현재 탭의 id와 동일하도록 index에 1을 더한 값으로 업데이트 하고, 업데이트 된 값과 이미지의 id가 일치할 경우 해당되는 이미지만 렌더링 되도록 구현했다.

그리고 선택된 탭의 index에 1을 더한 값과 최신 상태의 currentTab이 일치할 경우, 활성화 된 탭의 폰트가 bold 처리 될 수 있도록 스타일 코드를 삼항 연산자로 스타일링 했다.

고심해서 구현했지만 반복되는 코드가 있는 것 같고, 더 간단하게 리팩토링 할 수 있을 것 같아 고민을 더 해봐야 할 것 같다.

 

  • SUSTAGRAM Delete, Undo 구현하기
  1. delete 버튼 클릭 시 이미지가 삭제되도록 구현
  2. [복구하기] 버튼 생성
  3. [복구하기] 버튼 클릭 시 이미지가 삭제 된 순서대로 되돌리도록 구현
import { useState } from 'react';

function App() {
  const [pictureList, setPictureList] = useState([
    'https://cdn.pixabay.com/photo/2013/08/26/09/40/silhouette-175970_1280.jpg',
    'https://cdn.pixabay.com/photo/2015/11/25/09/42/rocks-1061540_1280.jpg',
    'https://cdn.pixabay.com/photo/2018/09/23/12/33/building-3697342_1280.jpg',
    'https://cdn.pixabay.com/photo/2014/05/02/12/43/clouds-335969_1280.jpg',
    'https://cdn.pixabay.com/photo/2022/12/28/21/10/streets-7683842_1280.jpg',
    'https://cdn.pixabay.com/photo/2023/01/08/05/45/mountain-7704584_1280.jpg',
  ]);

  const [deletedPictures, setDeletedPictures] = useState<string[]>([]);

  const deleteHandler = (index: number) => {
    const deletedPicture = pictureList[index];
    setPictureList((currentPictureList) =>
      currentPictureList.filter((_, i) => i !== index)
    );
    setDeletedPictures((prev) => [...prev, deletedPicture]);
  };

  const restoreHandler = () => {
    if (deletedPictures.length > 0) {
      const lastDeletedPic = deletedPictures[deletedPictures.length - 1];
      setDeletedPictures((prev) => prev.slice(0, -1));
      setPictureList((prev) => [...prev, lastDeletedPic]);
    }
  };

  return (
    <div>
      <header>
        <h1>SUSTAGRAM</h1>
        <button
          type="button"
          onClick={restoreHandler}
          className={`bg-[#ef4444] ${
            deletedPictures.length === 0 && 'bg-[#DDDDDD] text-[#999999]'
          }`}
          disabled={deletedPictures.length === 0}
        >
          사진 복구하기
        </button>
      </header>
      <div>
        {pictureList.map((value, index) => (
          <div className="relative group" key={value}>
            <a className="group" href="#">
              <img
                src={value}
                width="400"
                height="400"
                alt={`Photo ${index + 1}`}
              />
            </a>
            <button onClick={() => deleteHandler(index)}>
              <img src="/delete.svg" alt="Delete icon" />
              <span className="sr-only">Delete</span>
            </button>
          </div>
        ))}
      </div>
    </div>
  );
}

export default App;

 

delete 기능을 구현하기 위해 초기값으로 구성된 pictureList 에서 최신 상태를 복사한 후,

filter 메서드를 통해 현재 클릭된 index와 복사해온 index가 일치하지 않은 나머지 값들을 구성하여 새로운 pictureList 상태로 업데이트 한다.

undo 기능을 구현하기 위해 delete 기능에서 현재 선택된 index에 해당하는 값을 deletedPictures 라는 상태 값에 업데이트 시키고, 기존에 추가된 값에 계속해서 추가될 수 있도록 ...prev를 사용한다.

deletedPictures의 값이 0 이상일때만 작동하도록 조건을 추가하였고, deletedPictures 상태의 마지막 요소를 lastDeletedPic으로 추출하여 복구할 이미지로 설정한다.

deletedPictures의 현재 상태 값을 lastDeletedPic을 제외한 나머지 이미지로 업데이트 하고, 복구한 이미지는 pictureList에 추가한다.

[사진 복구하기] 라는 버튼을 새로 생성하여 undo 기능이 포함된 핸들러 함수를 연결시키고, deletedPictures에 이미지가 없을 경우 disabled 상태가 되도록 스타일링 한다.

 


 

점점 실습을 많이 하게 되면서 어떻게 기능을 구현해야할지 고민하는 과정에서 많이 배우는 것 같다.

그 과정에서 어떻게 더 간단하고, 성능을 고려하는 코드로 리팩토링 할 수 있을지 고민하는게 앞으로의 내 과제일 것 같다는 생각이 들었다.

점점 성장하고 있는게 느껴져서 뿌듯하다!!

 

사전직무교육 1주차 동안 다사다난했지만 고생 많았고, 다음주에도 열심히 달려보자!!! 😇


본 후기는 본 후기는 [유데미x스나이퍼팩토리] 프로젝트 캠프 : React 2기 과정(B-log) 리뷰로 작성 되었습니다.