React.js 입문

ANN·2일 전

OneBiteReact

목록 보기
4/4
post-thumbnail

한 입 크기로 잘라 먹는 리액트(React.js) : 기초부터 실전까지 by 이정환

📌  실습 준비하기

프로젝트 설치

npm create vite@latest

앱 프로젝트 만들기

로컬 환경에서 웹 서비스를 실행할 수 있음

웬만하면 설치하여서 사용 권장

실습을 위한 옵션 몇 가지

일단 rules 부분만 확인해서 내용 수정

rule: {
      "no-unused-vars": "off",
      "react/prop-types": "off",
},
  • 사용하지 않는 변수는 오류로 나타내지 않기
  • React 컴포넌트에서 Prop Types를 정의하도록 요구하기

📌 React 컴포넌트

리액트에서는 자바스크립트로 짠 함수가 html 태그를 반환하도록 설정 가능
➡️ 이렇게 html 태그들을 반환하는 함수를 특별히 컴포넌트라고 함

그리고 컴포넌트를 부를 때는 함수의 이름을 따서 부름
➡️ App 컴포넌트

직접 컴포넌트 만들어보기

위와 같이 Header 컴포넌트를 만들어봄

이것은 아래와 같이

화살표 함수로 만들 수 있음

함수 말고 클래스를 이용해서 만들 수 있지만 함수로 컴포넌트를 만드는 게 훨씬 일반적이고 대중적임

또한 컴포넌트는 첫 글자가 대문자
그래야 리액트에서 컴포넌트라고 인정

만든 컴포넌트 렌더링

이제 Header를 어떻게 렌더링?

html 태그를 작성하듯이 컴포넌트 배치
➡️ App 컴포넌트가 화면에 렌더링될 때 Header 컴포넌트의 리턴문도 불러와서 같이 렌더링

새로 만든 Header 컴포넌트처럼,
다른 컴포넌트의 리턴문 내부에서 포함되는 이러한 컴포넌트를 "자식 컴포넌트"라고 함

반대로 App 컴포넌트는 부모 컴포넌트

이런 식으로 계층 구조로 컴포넌트 간의 표현 가능

만약 만들었던 Header 컴포넌트 외의 컴포넌트(Main, Footer...) 화면에 렌더링하기 위해서는
Header 컴포넌트처럼 App 컴포넌트의 자식 컴포넌트로 배치해야 함

결론적으로,
리액트의 모든 컴포넌트들은 화면에 렌더링되기 위해서
앱 컴포넌트의 자식 컴포넌트로서 존재해야 함

➡️ 모든 리액트 컴포넌트는 앱 컴포넌트를 최상위, 조상으로 갖는 계층 구조를 갖게 됨

그리고,
이 모든 컴포넌트의 조상 역할을 하는 앱 컴포넌트는
특별히 모든 컴포넌트의 뿌리 역할을 한다고 해서 루트 컴포넌트라고 함

루트 컴포넌트는 main.jsx라는 파일에 렌더 메서드의 인수로 전달된 컴포넌트이기 때문에,
➡️ 원하는 대로 변경 가능

위와 같이 루트 컴포넌트로 Hello 컴포넌트로 설정

하지만 관례상 App이라는 이름을 갖는 컴포넌트를 루트 컴포넌트로 설정

컴포넌트 폴더별로 분리

모듈화를 위해 컴포넌트별로 각 파일에 나눠 작성

src 폴더 아래에,
루트 컴포넌트인 App 컴포넌트 외 추가적인 컴포넌트를 모아두는 components라는 폴더 생성

Hello 컴포넌트의 경우 루트 컴포넌트가 아니므로,
components 폴더 아래에 새로운 파일인 Header.jsx 생성
App에 들어있던 Header 컴포넌트 코드를 복사해서 저장

Header 컴포넌트가 파일로 분리되었으니
렌더링하려면 import문 추가

vite로 만든 리액트 앱에는 import문에 확장자를 쓰지 않아도 됨
ex) import Header from "./components/Header.jsx"

정리

위와 같이 렌더링하고자 하는 컴포넌트들이 있다면,

src/components 폴더 아래에 해당 파일들을 저장해두고,

import문과 함께 렌더링할 수 있음


📌 JSX - UI 표현하기

JSX

리액트 컴포넌트가 화면에 나타나는 UI 요소들을 표현할 수 있도록 도와줌

JSX : 확장된 자바스크립트 문법
➡️ 일종의 자바스크립트 확장판

JSX는 자바스크립트 코드 안에서 html 코드를 삽입 가능하게 해줌
➡️ 직관적이고 가독성 높은 컴포넌트 생성 가능

이와같이,
리액트 컴포넌트는 자바스크립트와 html 태그를 혼용해서 사용 가능

또한, 단순히 html 태그를 반환하는 것을 넘어서,

(강의자료 감사)

위 자료와 같이 동적으로 특정 변수의 값을 html로 렌더링할 수 있음

실습

위처럼 동적으로 특정 변수의 값을 렌더링하면 아래와 같은 화면을 볼 수 있음

또한, JSX는 중괄호 안에 다양한 타입이나 연산식을 넣고 렌더링할 수 있음

주의할 점

중괄호 안에는 자바스크립트 표현식만 넣을 수 있음

자바스크립트 표현식이란?

삼항 연산자, 변수, 값, 한 줄의 코드가 특정한 값으로 평가될 수 있는 코드

즉, 중괄호 안에 if문, for문 같은 제어문은 오류 발생
왜냐하면, 한 줄로써 값으로 평가될 수 없기 때문에

또한, JSX에서 숫자, 문자열, 또는 배열의 값만 정상적으로 렌더링
boolean, undefined, null 과 같은 값은 오류를 발생하진 않지만, 화면에 렌더링되지 않음


메인 컴포넌트 안에 객체 값을 그대로 렌더링하면,
➡️ 그냥 브라우저 화면이 백지가 됨 = 오류 발생

이때는, 개발자 도구로 확인하면 오류 확인 가능

객체는 리액트의 자식으로 유효하지 않아서, 렌더링 불가
하지만 점 표기법을 이용해서 프로퍼티는 렌더링 가능

모든 태그는 닫혀야 함

즉, <h1>이 있으면 </h1>도 있어야 함
html에서는 <img> 같은 건 닫히지 않은 상태여도 괜찮지만
JSX는 모두 닫혀 있어야 해서, 닫힘 태그나 셀프 클로징 태그를 달아야 함

최상위 태그는 하나여야 함

보면, <div><main> 태그가 최상위에 같이 있는데
➡️ 오류 발생

최상위에은 하나의 태그만 존재해야 오류가 발생하지 않음

최상위 태그로 쓸만한 마땅한 태그가 없다면,
빈 태그 활용
➡️ JSX는 최상위 태그가 있다고 판단하지만,
실제 렌더링될 때는 최상위 태그가 없는 것처럼 렌더링 됨

활용

삼항연산자 활용

컴포넌트가 조건에 따라 각각 다른 UI를 렌더링

user.isLogin 조건에 따라 로그인을 출력하거나, 로그아웃을 출력

조건문 활용

(같은 렌더링 화면인데, 코드는 짜기 나름)

스타일 적용

DOM요소에 직접 스타일 속성 설정

스타일 객체로 설정
➡️ 객체니까 중괄호 두 번

프로퍼티를 쓸 땐 카멜 케이스로 backgroundColor로 작성

JSX에서는 DOM 요소의 스타일 속성을 직접 설정하여 스타일링 가능

그러나 위와 같이
리턴문 안에 스타일링 코드를 직접 작성하면,
스타일 규칙이 많아질수록 가독성이 떨어짐(그럼 tailwind는...?)
➡️ 별도의 CSS 파일을 만들어 스타일링 가능

CSS 파일 생성

Main.css 파일

Main.css 파일을 import
<div>태그를 보면 class가 아닌 className이란 속성으로 클래스 설정

원래 HTML에선 class라고 했지만,
지금은 자바스크립트와 HTML을 같이 쓰고 있고,
자바스크립트의 예약어인 class를 쓸 수 없으므로
className으로 설정해야 함


📌 Props로 데이터 전달하기

Props란?

리액트 컴포넌트에게 값을 전달하는 방법

리액트에서는 페이지를 컴포넌트 단위로 나누어서 레고를 조립하듯 개발

메뉴 버튼을 보면
버튼은 다 똑같이 생기고,
안에 그려진 아이콘만 다름

버튼 컴포넌트를 만들어서 반복적으로 렌더링하면 됨
➡️ 버튼 컴포넌트는 하나만 만들고,
이미지와 텍스트만 바꿔가며 여러 번 반복해서 렌더링

메뉴 버튼처럼 구조는 같지만, 세부적인 내용만 조금씩 다르게 렌더링하도록 설정해주기 위해서는 각각의 버튼 컴포넌트들이 어떤 버튼(아이콘)을 렌더링할 것인지, 결정하는 값을 전달해야 함

마치 함수를 호출하면서 인수를 전달하듯이...

텍스트와 이미지로 원하는 아이콘을 렌더링

  • 텍스트로 "메일", 이미지로는 "mail.png"
  • 텍스트로 "카페", 이미지로는 "cafe.png"
  • 텍스트로 "블로그", 이미지로는 "blog.png"

리액트에서는 부모 컴포넌트가 자식 컴포넌트에게 마치 함수의 인수를 전달하듯이 원하는 값 전달 가능

이렇게 컴포넌트에 전달된 값을 props라고 함
(Properties의 줄임말)

정리

이렇게 props를 이용하면,
컴포넌트를 마치 함수를 호출하듯이 전달하는 값에 따라서
각각 다른 UI를 렌더링하도록 만들 수 있기 때문에
props는 리액트의 핵심 개념

실습

버튼 컴포넌트 생성

버튼 컴포넌트 렌더링

현재는 이 버튼 컴포넌트에 별다른 props를 주지 않았으므로,

버튼 컴포넌트만 그대로 3번 렌더링

props 전달

버튼 컴포넌트에, 전달받은 props를 출력하도록 수정

버튼 컴포넌트를 렌더링하면서, props를 전달

세 개의 버튼 컴포넌트를 렌더링했기 때문에,
출력도 세 개
그리고, props의 값이 객체 형태로 담겨서 전달됨

props라는 매개변수에 객체 형태로 저장되어 있으니까,
점 표기법을 사용해서 렌더링하도록 설정 가능

그럼, 해당 프로퍼티의 값이 렌더링 되는 것 확인


넘겨받은 props로 스타일도 지정 가능

color를 받은 버튼은 해당 컬러로 렌더링이 되지만,
받지 못한 버튼은 기본값인 검정으로 렌더링됨

그리고 해당 값을 버튼의 텍스트로 렌더링하면,

두 번째, 세 번째 버튼은 props로 컬러를 전달해주지 않아서 -까지만 렌더링

모양새도 별로지만
props로 color 값이 무조건 들어올 거라고 예상하고, 코딩하다가 오류가 발생할 수도 있음


color props가 없어서, 값이 전달되지 않으면 undefined가 되는데,
여기에 점 표기법을 쓰고 toUpperCase라는 메서드를 호출하려니까 오류 발생

자동으로 설정될 기본값을 설정해서 오류를 해결

(최신의 리액트에서는)

이렇게 props를 전달받을 때 props의 기본값을 설정할 수 있음

즉, props의 기본값을 설정할 때는
props를 구조분해할당 문법으로 받아오게 하고,
구조분해할당 문법에서 기본값을 입력하여 props의 기본값 설정

스프레드 연산자 전달

여러 개의 값을 props로 전달해야 한다면,
그 값을 하나의 객체로 묶어서 스프레드 연산자를 통해 한 방에 전달하도록 할 수 있음


또한, props에는 문자열이나 숫자 같은 자바스크립트 값뿐만 아니라
html요소나, 리액트 컴포넌트도 전달 가능

html요소는 Button 컴포넌트에 children이라는 이름의 props로 자동으로 전달
구조분해 할당 문법에서 children이라는 이름으로,
자식 요소라는 <div>태그를 받아옴


또한 html뿐만 아니라, 컴포넌트도 children props로 전달하는 게 가능

컴포넌트를 받아와 렌더링할 수 있음


📌 이벤트 처리하기

이벤트 핸들링이란?

이벤트란?
웹 내부에서 발생하는 사용자의 행동
ex) 버튼 클릭, 메세지 입력, 스크롤 등등

핸들링이란?
다루다는 뜻
➡️ 이벤트가 발생했을 때 그것을 처리하는 것

버튼을 클릭했을 때, 어떠한 동작을 수행하게 하려면

(예시)

버튼 클릭 시, 콘솔 로그로 props로 받은 메뉴 이름 출력

이런 식으로 클릭과 같은 이벤트가 발생했을 때 처리(실행)할 수 있도록 설정된 함수를,
이벤트 핸들러라고 함

이벤트 핸들러를 설정하는 법

  • onClick 내에 익명 함수로 설정
  • 함수를 선언하여 해당 함수 전달

화살표 함수(혹은 선언문) 하나를 만들어서,
버튼이 클릭되었을 때 실행할 명령문을 기재

따라서, onClickonClickButton이란 함수가 설정이 되면서,
이 버튼의 클릭 이벤트 핸들러로
onClickButton이란 함수가 설정된 것

주의

이벤트 핸들러를 설정할 때,
호출 결과를 전달하지 말 것

위 코드는 함수의 결과(반환값 등)를 전달하는 것


클릭 이벤트 외에 다른 이벤트도 마찬가지임


리액트에서 발생하는 모든 이벤트는,
이벤트 핸들러 함수를 호출하면서,
호출된 이벤트 핸들러 함수에 매개별수로, 이벤트 객체라는 것을 제공

따라서 객체 e라는 매개변수를 출력해보면,

SyntheticBaseEvent라는 객체가 출력
= 매개변수 e에 저장된 이벤트 객체

SyntheticBaseEvent의 Synthetic은 합성이란 뜻으로,
합성 이벤트 객체, 이런 뜻

합성 이벤트란,

모든 웹 브라우저의 이벤트 객체를 하나의 포맷으로 통일한 형태

크롬, 사파리, 엣지, 웨일 등...
브라우저를 만드는 회사는 굉장히 많음
= 웹 브라우저마다 동작이 조금씩 다름

가령, IE는 최신 자바스크립트 기능은 거의 쓸 수 없음


내가 쓰려는 자바스크립트나, CSS의 기능이 어느 브라우저에서 되는지 확인하는 사이트도 있음

이러다 보니까 이벤트 객체도 브라우저마다 조금씩 다름

예를 들면, 크롬에서는 이벤트가 발생한 요소를 가져오는 타겟이라는 속성을, 다른 브라우저에서는 다르게 부를 수도 있다는 것임

따라서 크로스 브라우징 이슈를 해결해주는 친구가
리액트의 합성 이벤트

여러 브라우저의 규격을 참고해서 하나의 통일된 규격으로 이벤트 객체를 포맷팅
➡️ 일종의 통합 규격

따라서 리액트 개발자는 브라우저별 이벤트가 다른 건 신경쓸 필요가 없고
이 합성 이벤트 객체만 잘 사용하면 됨

정리

SyntheticBaseEvent객체란, 모든 브라우저에서 사용할 수 있는 통합된 규격의 이벤트 객체

따라서, 이 합성 이벤트 객체 안에는 발생한 이벤트와 관련된 모든 정보가 다 들어있음
➡️ 리액트에서는 이러한 합성 이벤트 객체를 제공하기 때문에 크로스 브라우징 이슈에서도 비교적 자유로움


📌 State로 상태관리

State란?

어떤 사물이 현재 가지고 있는 모양이나 형편
모든 사물은 자신이 갖는 현재의 상태, 즉 현재의 state에 따라서 각각 다른 모양이나 다른 동작을 함

만약 전구를 끄면, 켜진 상태의 전구 ➡️ 꺼진 상태의 전구
만약 전구를 켜면, 꺼진 상태의 전구 ➡️ 켜진 상태의 전구

이렇듯 state, 상태란 어떠한 사물이 현재 가지고 있는 모양이나 형태를 정의하는 값이면서,
동시에 변화할 수 있는 동적인 값

리액트의 컴포넌트는 자신의 형태나 모양을 정의하는 이런 state를 가질 수 있음
➡️ state 값(현재의 상태)에 따라, 각각 다른 UI를 화면에 렌더링


예를 들면, 전구를 렌더링하는 컴포넌트를 만듦
이 전구의 state 값이 off면, 꺼진 상태의 전구 렌더링

이 전구의 state 값이 on으로 바뀌면,
리액트가 state의 값이 바뀐 걸 감지해서 컴포넌트를 다시 렌더링

따라서, 켜진 상태의 전구가 렌더링됨

➡️ 이렇게 컴포넌트가 다시 렌더링되는 걸 리렌더 또는 리렌더링이라고 함


결론적으로 리액트에서는
각각의 컴포넌트에 이 컴포넌트의 상태를 의미하는 값이자,
변할 수 있는 값인 state를 만들 수 있으며,
하나의 컴포넌트에 여러 개의 state를 만드는 것도 가능

실습

두 개의 요소가 들어간 배열 출력

  • 첫 번째 요소 : 값이 없음
    • 새로운 state 값
    • 지금은 초기값을 넣지 않아서 undefined
  • 두 번째 요소 : 함수
    • state 값을 변경하는, 즉 상태를 변화시키는 함수가 들어 있음
    • 상태 변화 함수라고 부름

새로운 state를 생성하는 useState라는 함수는,
인수로는 state의 초기값을 받아서
➡️ 두 개의 요소를 담은 배열 반환
그리고 배열의 구조 분해 할당 문법을 이용하여 바로 사용

따라서 할당받은 변수로 바로 사용할 수 있음

가령, 아래처럼 구조 분해 할당 문법을 사용하지 않고 바로 변수에 저장하면

const result = useState(0); 
// result는 [현재값, 값을 업데이트하는 함수]가 들어있는 배열

각 요소에 접근하기 위해 result[0], result[1] 이런 식으로 써야 함

버튼을 만들어서, 클릭할 때마다 state를 증가시키기

컴포넌트 내에 새로운 state 생성
버튼을 클릭했을 때 state값을 변경하도록 하면,
➡️ 리액트가 내부적으로 이 state의 변경 여부를 감지해서
이 컴포넌트를 리렌더링

함수 컴포넌트를 리렌더링한다는 말은,
이 컴포넌트 역할을 하고 있는 App 함수를 다시 호출하고,
새롭게 반환한 값을 화면에 다시 렌더링

리렌더링 동작 과정 정리

- App 컴포넌트가 최초로 렌더링될 때는 state 값이 초기값인 0
- 클릭해서 state 값이 업데이트가 되면 App 컴포넌트의 역할을 하는 함수가 다시 호출됨
- 그러면 state값이 1인 상태로 즉, 업데이트가 된 상태를 다시 리턴문을 통해서 반환
- 업데이트된 state의 값이 화면에 즉각적으로 반영

단순하게 따지면
컴포넌트의 state 값이 바뀌면 컴포넌트가 값을 리턴하고,
화면을 다시 그림

새로운 state : 전구의 on/off를 저장하는 state 만들기

위와 같이 새로운 state를 추가하고,
버튼을 누르면 해당 state의 상태값을 ON/OFF로 바꾸는 이벤트 핸들러 추가


상태에 따라 버튼의 텍스트도 변경 가능


useState 대신 let, const로 자바스크립트 변수를 만들면?

할 수는 있음

그러나 변수로 전구의 ON/OFF 상태를 저장하면,
버튼을 클릭해서 변수의 값이 바뀌어도 컴포넌트가 리렌더링 하지 않음
➡️ 따라서 버튼을 클릭해도 변화가 일어나지 않음

리액트 컴포넌트는 일반적인 변수가 아니라 state 값이 변화했을 때만 리렌더링


📌 state를 props로 전달하기

전구 불 밝히기

전구의 색상을 바꾸는 부분을 컴포넌트로 분리
버튼을 눌러 App 컴포넌트의 state를 바꾸면,
Bulb 컴포넌트가 리렌더링
➡️ Bulb와 같은 자식 컴포넌트는, 부모로부터 받는 props의 값이 바뀌면 리렌더링 발생

const Bulb = ({ light }) => {
  console.log(light); // light 상태 확인 콘솔 로그 추가
  return (
    <div>
      {light === "ON" ? (
        <h1 style={{ backgroundColor: "orange" }}>ON</h1>
      ) : (
        <h1 style={{ backgroundColor: "gray" }}>OFF</h1>
      )}
    </div>
  );
};

버튼 클릭 시 ➡️ props로 제공되는 라이트의 값이 변경 ➡️ Bulb 컴포넌트 재호출 ➡️ 전구 다시 렌더링

근데 카운터도 같이 증가 중

클릭해서 카운트를 올리는 count state 값만 변경되길 기대했는데,
(Bulb에게 props로 전달한 light state의 값을 변경한 게 아닌데)

+를 클릭하면, 콘솔로그로 light의 상태를 출력하면서, Bulb 컴포넌트가 리렌더링 되는 것인가?

리액트 컴포넌트가 리렌더링하는 세 가지 상황

  1. 자신이 관리하는 state의 값이 변경될 때
  2. 제공받는 props의 값이 변경될 때
  3. 부모 컴포넌트가 리렌더링되면, 자식 컴포넌트도 리렌더링
    ➡️ 그래서 지금 Bulb 컴포넌트가 계속 리렌더링

+ 버튼을 클릭하면 App 컴포넌트의 count state의 값이 변경됨
1번 조건에 따라, App 컴포넌트는 자신이 가진 state의 값이 변경되어서 App 컴포넌트가 리렌더링

Bulb 컴포넌트는 App 컴포넌트의 자식 컴포넌트이므로,
3번 조건에 따라 App 컴포넌트가 리렌더링되어, Bulb 컴포넌트도 리렌더링 발생


Bulb 컴포넌트는 count의 증가와는 아무 관련이 없는데
부모 컴포넌트가 리렌더링됨으로써 불필요하게 리렌더링 되는 중
➡️ 이와 같은 자식 컴포넌트들이 불필요하게 리렌더링되면, 성능이 저하될 수밖에 없음

컴포넌트 분리

이렇게 관련 없는 두 개의 state는 하나의 컴포넌트에 몰아두지 말고 서로 다른 컴포넌트로 분리

counter 컴포넌트

Counter 컴포넌트를 분리하고, 각 컴포넌트가 state를 가짐

이후, 각 컴포넌트가 state를 각자 관리하므로
불필요한 리렌더링을 막음

파일 분리

각 컴포넌트는 파일별로 분리할 수 있음


📌 state로 사용자 입력 관리하기 1

회원가입 폼을 렌더링하는 컴포넌트 만들기

컴포넌트 준비

App 컴포넌트와

회원가입 폼을 위한 컴포넌트


이름 입력받기

콘솔 로그로 이벤트 객체를 출력하면,

위와 같이 출력되고,
이 이벤트 객체에는 우리가 찾고 있는 inpout에 사용자가 입력한 값이 그대로 있음

➡️ 즉, e.target.value 안에는 input에 입력한 텍스트가 있음


최종적으로,
입력한 값들이 위 코드의 로직을 통해 name state에 저장될 것


만약 input 폼 안에 초기값을 설정하고 싶다면
name이란 state를 생성할 때, 초기값을 넣으면 됨

그리고 name을 input의 value로 설정

보통 리액트의 상태를 이용해서,
사용자의 입력을 저장하고 처리할 때에는
이렇게 초기값을 설정하는 경우도 꽤 많이 존재
➡️ onChange 속성만 설정하지 말고,
value 속성까지 함께 설정해야 함

생년월일 입력받기

날짜 선택기인 date picker 렌더링
(이 모양은 브라우저에 따라서 다를 수 있음)


생년월일 input에 사용자가 입력하는 값도 state를 통해서 보관


국적 입력받기

국적은 셀렉트 박스로 입력받기

초기값으로 한국 설정
셀렉트 태그는 기본적으로 옵션 중에 맨 위 옵션을 초기값으로 사용하기 때문


만약 아무것도 선택되지 않은 상태로 초기값을 설정하고 싶은 경우 빈 옵션 초기값으로 설정


이제 셀렉트 태그에 사용자들이 선택하는 값도 state를 통해 보관


전체 적용 코드


셀렉트 박스의 경우
화면에 실제로 표시되는 선택지와, 실제 코드 상에서 사용할 value 값을 다르게 선택 가능

한국이라는 option의 value를 kr로 설정하면,
한국을 선택했을 때 실제로 코드 내에서는 kr이라는 value로 저장됨

따라서 선택지에는 친절하고 길게 텍스트를 명시하고 내부적인 value로는 더 간결한 값을 사용하는 경우가 많음


자기소개 폼

input 폼이 아닌 textarea 폼으로 입력 받음

input 태그와 달리 여러 줄을 입력 가능
그러나 내부 동작은 input 태그와 완전히 같음



회원가입 폼 완성


📌 state로 사용자 입력 관리하기2

위 섹션에서는 Register 컴포넌트 안에
네 가지 정보를 입력 받는 폼을 각각 만들고
해당 폼에 입력되는 정보를 각각 새로운 state를 생성하여 따로따로 보관

따라서 코드를 보면 비슷한 부분이 굉장이 많음

모든 이벤트 핸들러에서 set함수로 상태 변화 함수를 호출하고,
인수로는 e.target.value를 똑같이 전달하고,
4개의 state가 사실상 같은 방식으로 활용되고 있음

이 방식이 반복되니 비효율적이라고 판단됨


모든 state를 하나의 객체로 묶어 관리하면,
여러 개의 state를 만들지 않아도 될 것 같고...

이벤트 핸들러도 하나로 합쳐볼 수 있을 것 같고...


리팩토링 해보자

상태 정리

(위에 써있는 대로)
새로운 객체를 생성해서,
name 프로퍼티의 값을 수정하고,
그 외에 기존의 input state에 들어있던 프로퍼티 값들(birth, country)를
스프레드 연산자를 이용해서 기존의 값 유지
➡️ 변경하고자 하는 값만 바꿔주도록 코드 작성

name만 바꾸고 싶어도 setInput({ name: 새값 })처럼 일부만 넣어서 업데이트하면
React는 객체를 “합쳐서” 업데이트해주지 않고 통째로 교체

...input을 빼면

setInput({ name: newName });

이렇게 되면 state가 아예 { name: newName }만 남고,
기존에 있던 birth, country, bio는 객체에서 없어져버림

name만 바꾸고 싶어도 일부만 넣어서 업데이트하면
React는 객체를 “합쳐서” 업데이트해주지 않고 통째로 교체
➡️ 기존의 값들은 유지하고, 변경하고자 하는 값만 바꿔주도록 코드 작성


전체적으로 코드에 적용한 모습


적용한 모습.
일단 빈 객체를 출력


이름을 쓰자. 이름을 입력하는 만큼 name 프로퍼티가 업데이트되는 것 확인


생일 프로퍼티를 바꿨지만,
name 프로퍼티는 그대로 유지되었음


이렇게 여러 개의 state로 나눠서 관리하던 네 가지의 데이터를,
객체 형태로 만들어서 하나의 state로 관리하는 방법에 대해 배웠음
➡️ 나중에 복잡한 상태를 직접 관리해야 할 때 큰 도움이 될 것

핸들러 정리

여전히 핸들러는 4개가 존재하고 있음

이벤트 핸들러를 다 지워주고 아래와 같은 함수를 작성

e.target.name : 그 input의 name 속성 값 (예: "name", "birth", "country")
e.target.value : 사용자가 입력한 값

<input name="name" value={input.name} onChange={onChange} />
<input name="birth" value={input.birth} onChange={onChange} />
<input name="country" value={input.country} onChange={onChange} />

input의 name 속성 값은 그냥 HTML에서 input 태그에 붙이는 “이 입력칸의 키(이름표)”임

따라서, 해당 input 태그에 붙은 이름표를 프로퍼티의 키, 입력한 값을 밸류로 받아 set함수를 통해 상태를 업데이트함


이렇게 작성하니 전체적인 코드가 깔끔해졌음

동작원리

모든 input, 모든 select, 모든 textarea 태그의 onChange 이벤트 핸들러를
방금 만든 통합 이벤트 핸들러 onChange로 설정했기 때문에
이제 어디에 어떤 값을 입력하든 이 onChange가 실행

이 함수가 실행되면 setInput 상태 변화 함수를 호출하고,
인수로 객체를 만들어서 전달
스프레드 연산자로 기존 input의 값을 나열하고,
마지막에는 프로퍼티의 키를 명시하는 자리에 대괄호를 열고
이 대괄호 안에 e.target.name을 넣음

e.target.name에는 뭐가 있을까?
이벤트가 발생한 태그의 name 속성에 설정된 값이 들어있음


만약 브라우저에서 생년월일을 입력하면

코드 상에서 위 input 태그의 이벤트 발생

이때 이벤트의 타겟, 즉 e.target은 input 태그이고,
이 태그의 name은 birth
➡️ 그래서 birth: e.target.value처럼 코드가 작성하는 것


입력 이벤트의 name과 value를 출력해보자

name 속성에 입력하는 경우 이벤트 발생 로그

birth 속성에 입력하는 이벤트 발생 로그


📌 useRef로 컴포넌트의 변수 생성하기

useRef

새로운 Reference 객체를 생성하는 기능

const refObject = useRef();

이렇게 생성한 레퍼런스 객체는 컴포넌트 내부의 변수로서 일반적인 값들 저장
그래서 어쩌면, useState와 비슷
➡️ 두 기능 모두 우리가 컴포넌트 내부에서 쓰고 싶은 변수를 생성한다는 점에서 동일

값이 변경되었을 때 컴포넌트를 리렌더링 시키는 useState와 달리,
useRef로 생성한 변수는 값이 변경되더라도 컴포넌트를 리렌더링시키지는 않음
➡️ 렌더링에 영향을 미치고 싶지 않은 변수를 생성할 때 useRef 이용

또한, useRef를 이용하면, 컴포넌트가 렌더링하는 특정 DOM 요소에 접근 가능
➡️ 해당 요소 조작이 가능

위처럼, 특정 요소에 포커스를 준다든지,
혹은 해당 요소의 스타일을 갑자기 변경시키는 동작도 손쉽게 구현 가능

실습1️⃣

방금 생성한 레퍼런스 객체

결국 레퍼런스 객체란
current 프로퍼티에 현재 보관할 값을 담아두기만 하는 자바스크립트 객체


그렇기 때문에 useRef로 새로운 레퍼런스를 생성할 때
인수로 0이라는 초기값을 전달하면,

current프로퍼티에 초기값을 저장한 레퍼런스 객체 확인
이런 식으로 초기값을 설정하는 것도 가능

cuurent 같은 프로퍼티(=레퍼런스 객체의 값)를 사용하고 싶으면, 점 표기법으로 접근하면 됨

특징: 이 객체는 값이 변되었다고 컴포넌트를 리렌더링시키지 않음

ref+1 버튼을 클릭하면,

  • 온클릭 이벤트 핸들러로 refObj의 current 값을 1 증가
  • 해당 값을 출력

숫자가 증가하는 레퍼런스 객체의 값이 출력되지만
리렌더링을 유발하지 않기 때문에 onClick 이벤트 핸들러만 실행되는 중


리렌더링이 안 되는 걸 확인해보기 위해 아래와 같이 실습

레퍼런스 객체를 이렇게 선언하고, 페이지 새로고침

위에서 실습했던
ref+1 버튼을 만들고,
해당 버튼을 계속 클릭해도 이벤트 핸들러만 실행될 뿐
위 "Register 렌더링" 콘솔 로그는 출력되지 않음

➡️ refObj는 컴포넌트 내부에서 렌더링에 영향을 미치지 않아야 되는 변수를 생성할 때 활용할 수 있음


이제 이 레퍼런스 객체를 이용해서
레지스터 컴포넌트가 렌더링하고 있는 4개의 폼에, 사용자가 얼마나 많은 횟수의 변경을 일으켰는지
수정 횟수를 카운트하는 기능을 만들어보자

버튼 태그는 삭제하고,
레퍼런스 객체의 current의 숫자를 조정하였음

수정횟수를 잘 출력하는 것 확인

실습2️⃣

새로운 레퍼런스 객체를 생성해서,
이 컴포넌트가 렌더링하는 DOM 요소를 직접 조작
회원 가입 제출 기능을 구현하자


제출 버튼 생성


제출 시 이름을 제대로 입력했는지 확인할 것

만약 이름을 입력하지 않은 경우
이름을 입력하는 DOM 요소 포커스
➡️ return문 안에 있는 이름을 입력하는 DOM 요소인 input 태그에 접근할 수 있어야 함

어떻게 접근?
➡️ 레퍼런스 객체를 이용할 것


새로 만든 레퍼런스 객체를
input 태그의 ref 속성으로 넣어줌

이렇게 하면 input 태그가 렌더링하는 DOM요소가
inputRef라는 레퍼런스 오브젝트에 저장됨


이제 제출 버튼이 클릭되었을 때
inputRef.current 값을 출력하도록 작성

새로고침 후 제출 버튼을 누르면

콘솔 로그로 input 태그와 DOM요소가 잘 출력되는 것 확인
(근데 지금 봤는데 저기 왜 timeLog지?)


inputRef.current 값에 우리가 접근하고자 하는 DOM요소(input태그)가 저장되어 있다는 걸 아니까,
여기에 focus라는 메서드 호출

브라우저 새로고침 후 제출 버튼 클릭 시
이름을 입력하는 DOM요소의 포커스가 잘 실행되는 것 확인 가능

심화적인 내용

컴포넌트 내부에서 리렌더링을 유발하지 않는,
countRef 같은 변수를 만들고 싶다면,
사실은 useRef라는 기능을 이용하지 않아도

let count = 0

이렇게 변수로 나타낼 수 있지 않을까?

즉, 왜 굳이 useRef를 써야 하는가?

그 결과,

아무리 많은 수정을 해도 그냥 1만 출력
➡️ count 변수의 값이 1로 고정이 되어 있음


input 값을 입력했는데, 왜 저런 현상이 나타날까?

onChange 이벤트 핸들러가 실행되면서 state의 값을 변경
setInput으로 인하여 ➡️ 레지스터 컴포넌트가 리렌더링됨

컴포넌트가 리렌더링되면서,
컴포넌트 역할을 하는 레지스터 함수가 다시 호출되고
함수 안에 있는 코드도 다시 실행
➡️ let count = 0도 다시 실행
즉, 리셋되었다.

따라서, 브라우저에서 이름이나 생년월일을 수정하면
count 변수의 값은 0으로 다시 리셋
따라서 출력되는 값은 1로 고정

그러나 useRef나 useState를 이용해서 만든
리액트의 특수한 변수는 컴포넌트가 리렌더링 되어도 리셋되지 않음

따라서 컴포넌트 내부의 변수가 필요하다면,
let 키워드가 아닌,
useRef나, (렌더링에 영향 주고 싶으면) useState로 만들어야 함


그러면,
컴포넌트가 리렌더링될 때 count라는 변수가 초기화되지만 않으면 문제 없지 않음?
그럼 count 변수의 선언을 컴포넌트 외부에 한다면?

실제로 그러면 카운팅은 잘 됨

그러나 이게 좋은 방법도 아니고, 문제 해결법도 아님

왜냐하면,
레지스터 컴포넌트를 한 번만 렌더링하는 상황이면 문제가 없지만,

위와 같이 App.jsx의 앱 컴포넌트에서,
레지스터 컴포넌트를 두 번 렌더링하면 문제가 발생함

두 개의 레지스터 컴포넌트가 하나의 변수를 공유하게 됨

우리가 기대한 건 각각 컴포넌트별로 카운트를 따로 세는 거지,
위와 같이 하나의 변수를 공유하는 게 아님


앱 컴포넌트에서 레지스터 컴포넌트를 두 번 렌더링 한 건,
register.jsx 라는 자바스크립트 파일에 있는 레지스터 함수를 그냥 두 번 호출한 것

그러면 파일 전체가 두 번 실행된 게 아니라,
Register 함수만 두 번 호출
➡️ 따라서 count라는 변수는 한 번만 선언 되었고,
두 개의 함수가 똑같은 변수를 나눠 쓰고 있음


리액트에서는 특별한 경우가 아니면,
컴포넌트 외부에 변수를 선언하는 게 권장되지 않음

따라서 이러한 변수를 사용하고 싶다면, useRef를 이용하는 게 가장 좋다.

📌 React Hooks

📝 리액트 훅
클래스 컴포넌트의 기능을 함수 컴포넌트에서도 이용할 수 있도록 도와주는 메서드

2017년 이전에
과거의 리액트에서 대부분의 사람들은 클래스로만 컴포넌트를 만듦

Class 컴포넌트Function 컴포넌트
모든 기능을 이용할 수 있음UI 렌더링만 할 수 있음
ex) State, Ref, etc...

지금처럼 함수로도 만들 수 있었지만,
리액트의 모든 기능을 이용하려면 클래스 컴포넌트를 사용해야 해서
대부분은 클래스 컴포넌트 사용

근데 클래스 컴포넌트는,
클래스란 문법을 이용해야 해서
함수 컴포넌트에 비해 굉장히 복잡

문법이 간결한 함수 컴포넌트에서도 리액트의 모든 기능을 이용하고 싶었음

따라서, 리액트 훅이라는 기능 개발


함수 컴포넌트에서도, 클래스 컴포넌트의 기능을 마치 낚아채듯 가져와서 사용할 수 있게 하기에
➡️ 훅이라고 명명

그래서 굳이 문법이 복잡한 클래스 컴포넌트를 쓸 필요가 없고,
특별한 상황이 아니면 모든 컴포넌트를 다 함수 컴포넌트로 만듦

위에서 살펴본 useRef, useState 같은 리액트의 내장 함수는 사실 모두 리액트 훅이었음

  • useState : State 기능을 낚아채오는 Hook
  • useRef: Reference 기능을 낚아채오는 Hook

두 함수는 모두 use라는 접두사를 쓰고 있는데,
리액트 훅에는 이름 앞에 use라는 접두사가 붙는 특징이 있음

그리고 이 각각의 훅은 단수형인 Hook, 개념 자체를 가리키는 말은 Hooks라고 함


또한, 리액트 훅은 함수 컴포넌트 내부에서만 호출되고,
조건문, 반복문 내부에선 호출 불가

use라는 접두사를 이용해서 나만의 새로운 훅인 커스텀 훅을 만드는 것도 가능

실습

위와 같이 컴포넌트 준비

1. 훅은 함수 컴포넌트나 커스텀 훅 내부에서만 호출 가능

React Hook은 반드시 React 함수 컴포넌트 내부(또는 커스텀 훅 내부)에서만 호출할 수 있는데,
현재 최상위 스코프에서 훅을 호출하고 있음

브라우저에서도 경고가 뜸


따라서 이렇게 함수 컴포넌트 안에 써져야 함

2. 조건부로 호출될 수 없다.

훅은 조건문이나, 반복문 안에서 호출 될 수 없음
이는 리액트에서 절대 허용될 수 없음

왜냐하면,
리액트가 내부적으로 컴포넌트를 호출해서 화면에 렌더링할 때,
조건문이나 반복문 내부에서 훅을 호출하면
서로 다른 훅들의 호출 순서가 엉망이 되는 현상이 발생해서 내부적인 오류가 발생할 수 있음

따라서 훅은 컴포넌트 안에서만 호출

3. 나만의 훅, 커스텀 훅을 직접 만들 수 있다.

예를 들어서, 컴포넌트에 간단한 input을 하나 만들고,
staate로 이 input에 입력되는 값을 관리할 경우

  • 새로운 state 생성
  • 이벤트 핸들러 하나 생성
    • 이 이벤트 핸들러에서 매개변수 e를 받아서, setInput에 e.target.value를 전달하는 방식으로,
      state의 입력 값을 보관할 수 있었음
  • input 태그의 value에 input, 그리고 onChange로는 onChange설정

쪼끄만 input 하나 관리하는 코드가 너무 긺

그리고 이러한 컴포넌트가 엄청 많이 생성되어야 한다면,
각각의 컴포넌트마다 state를 만들고,
이벤트 핸들러를 만드는 코드를 중복으로 매번 작성해야 함


그래서 이 부분을 별도의 함수로 만듦

이렇게 만들면 될 것 같다!

그런데, 정작 사용하려면 오류가 발생

리액트 컴포넌트가 아닌 일반적인 자바스크립트 함수에서는 useState 같은 훅을 호출하면 오류가 발생
함수 컴포넌트가 직접 만든 커스텀 훅에서만 useState를 호출할 수 있기 때문

따라서 getInput 함수를 커스텀 훅이라는 우리가 직접 만든 리액트 훅으로 바꿔 주어야 함


함수의 이름 앞에 use만 써주면,
리액트는 내부적으로 use라는 접두사를 사용하는 함수를 커스텀 훅이라고 판단
해서,
또 다른 리액트 훅을 내부에서 호출해도 오류를 발생시키지 않음

(정말로 이름만 바꾸었다. getInput -> useInput)

따라서 이렇게 input같이 컴포넌트마다 반복되어서 동작하는 로직이 있고,
해당 로직이 useState와 같은 훅을 사용하는 로직이면,
그 로직은 커스텀 훅을 만들어서 분리해줄 수 있음


그렇게 분리된 로직은,
여러 번 반복해서 사용하는 걸 가능하게 함


훅은 파일을 분리해서 관리함

src/hooks라는 폴더 안에, Hook이라는 이름으로 보관하는 게 일반적임

그럼 함수 컴포넌트 내에는 이렇게 사용할 수 있음

0개의 댓글