04 리액트 컴포넌트

김혜진·2023년 1월 31일
0

리액트 퀵스타트

목록 보기
4/5

스타일


CSS 모듈

여러 컴포넌트에서 임포트 한 CSS 파일에 동일한 클래스명이 존재하면 충돌 발생
CSS는 먼저 임포트 한 것이 CSS가 위에 중첩되어 포개지면서 먼저 임포트한 것을 아래로 숨김

이렇게 여러 CSS 파일에서 동일한 이름의 클래스가 중첩될 때의 문제를 해결하기 위해 CSS 모듈 기능을 사용

  • 파일명의 확장자를 .module.css로 변경
  • 일반적인 컴포넌트나 객체처럼 임포트
import styleApp from './App.module.css'

그리고 CSS 클래스 이름 또는 CSS ID 이름을 마치 객체의 속성처럼 사용해 className 특성에 바인딩

<h2 className={AppCssModule.test}>Hello</h2>

CSS모듈을 사용하면 여러 컴포넌트에서 동일한 이름의 CSS 클래스를 포함한 CSS 파일을 임포트하더라도 충돌이 발생할 가능성이 없다.

styled-components

ES6의 태그된 템플릿 리터럴(tagged template literal) 문법을 사용해 컴포넌트에 동적인 CSS를 사용할 수 있게 하는 라이브러리

기존 CSS는 스타일을 정교하게 조작하기 쉽지 않으며 변수, 반복문, 함수 등의 프로그래밍 언어적인 특성이 없다.
이러한 점을 해결하기 위해 Sass와 styled-components 같은 여러 가지 방법이 있다.
styled-components를 이용하면 CSS 스타일의 문법을 그대로 사용하면서도 전달된 속성에 따라 스타일을 동적으로 구성할 수 있음

확장

styled-components 로 작성한 기존 컴포넌트를 활용해 스타일을 확장할 수 있다.

// styled-components로 작성한 컴포넌트 A
const A = styled.div`...`;
// A 컴포넌트를 확장한 컴포넌트 B
const B = styled(A)`...`;

B는 A의 스타일을 기본적으로 적용하고, 추가로 스타일이 적용된 컴포넌트



속성의 유효성 검증

컴포넌트에서 다음과 같은 내용을 확인할 수 있어야 함

  • 컴포넌트에서 사용할 수 있는 속성은 무엇인지
  • 필수 속성은 무엇인지
  • 속성에 전달할 수 있는 값의 타입은 무엇인지

검증 방법

  • 타입스크립트
    타입스크립트를 사용한다면 타입스크립트의 정적 타입 지원 기능 이용 가능
    이 방법은 컴파일(빌드) 시에 타입을 검사하며, IDE를 통해 코드 자동완성 기능을 지원받을 수 있다.

  • PropTypes
    PropTypes는 리액트가 지원하는 기능이며 컴파일 할 때가 아닌 실행 중에 속성에 대한 유효성 검증을 수행
    따라서 속성으로 전달하는 값과 타입에 따라 경고를 발생시킴

일반적으로는 타입스크립트의 정적 타입을 이용한 유효성 검증만으로도 충분하지만, 좀 더 엄격한 속성 유효성 검증이 필요하다면 두 가지를 병행하여 적용할 수 있다.


PropsTypes

npm install prop-types

PropTypes를 이용해 유효성 검증 시 사용할 수 있는 타입

  • PropTypes.array : 배열
  • PropTypes.bool : true/false의 불리언 타입
  • PropTypes.func : 속성을 이용해 함수와 메서드를 전달하는 함수 타입
  • PropTypes.number : 숫자 타입
  • PropTypes.obect : 객체 타입
  • PropTypes.string : 문자열 타입

좀 더 복잡한 유효성 검증 타입

  • PropTypes.instanceOf(Custom) : Custom 클래스의 인스턴스인지 검증
  • PropTypes.oneOf(['+', '*']) : [ ]에 포함된 값 중의 하나인지를 검증
  • PropTypes.oneOfType([PropTypes.number, PropTypes.string]) : [ ]에 포함된 타입의 값인지를 검증
  • PropTypes.arrayOf(PropTypes.object) : 객체의 배열인지 검증

이미 작성한 적이 있는 함수를 이용한 사용자 정의 유효성 검증 기능

const customValidator = (props: any, propName: string, componentName: string) => {};

첫 번째 인자 props는 컴포넌트로 전달된 속성
두 번째 인자는 x, y와 같은 속성의 이름, 전달된 속성의 값은 props[propName]과 같이 접근 가능
세 번째 인자는 컴포넌트 이름, 유효성 검증을 위한 함수를 여러 컴포넌트에서 사용할 경우, 컴포넌트 이름으로 컴포넌트를 식별

이 유효성 검증 함수가 Error 객체를 리턴하면 유효성 검증에 실패한 것으로 간주함
유효성 검증이 에러를 일으켜도 컴포넌트의 실행을 중단하지는 않는다.
단지 실행 중에 유효성 검증을 수행하고, 경고 메세지를 나타낼 뿐이다.
따라서 propTypes와 함께 실제 컴포넌트 내부 코드에서 유효하지 않은 값이 저달되었을 때의 예외 처리를 수행해야 함


기본값 지정

컴포넌트에 속성값이 전달되지 않으면 기본값이 주어지도록 설정할 필요가 있다.
이런 경우에 기본 속성(default props)를 지정

Calc.tsx

const Calc = (props: CalcPropsTypes) => {
  ...
}

Calc.defaultProps = {
  x: 100,
  y: 20,
  oper: "+",
}

export default Clac;

App.tsx

...

return (
  <div>
    <Calc x={x} />
  </div>
)

y, oper 속성을 넘겨주지 않더라도 기본값이 주어짐



리액트 이벤트

리액트가 이벤트를 처리하는 방법은 HTML DOM에서 이벤트를 처리하는 방법과 조금 다르다.
리액트는 HTML DOM 이벤트를 추상화하여 여러 브라우저에서 동일한 특성(attribute)을 이용할 수 있도록 이벤트를 정규화 함

또한 성능 개선을 위해 모든 이벤트를 리액트 컴포넌트 트리가 렌더링되는 루트 DOM 컨테이너 요소에 연결하고 이벤트를 위임(delegation) 처리 함
이벤트가 발생하면 리액트는 루트 DOM 컨테이너에서 적절한 컴포넌트 요소를 연결하여 실행

주의 사항

  • 이벤트 핸들러를 지정할 때는 카멜 표기법을 사용
  • 이벤트를 함수 또는 메서드와 연결할 때는 { } 보간법을 사용
  • DOM 요소가 아닌 컴포넌트에 이벤트를 설정할 수 없음

적용 방법

  1. 이벤트 핸들러 함수를 정의하여 { } 보간법을 이용해 외부함수를 바인딩
const eventHandler = () => { ... }

// JSX 내부에서 외부 함수 바인딩
<input type="text" ... onChange={eventHandler} />
// JSX 내부에서 익명 함수 바인딩
<button onClick={() => {...} }>버튼</button>
  1. 이벤트 핸들러 함수의 첫 번째 인자를 이용해 이벤트 아규먼트 값을 이용
const eventHandler = (e: ChangeEvent<HTMLInputElement>) => {
  setValue(e.target.value);
}

이벤트 아규먼트의 정적 타입

리액트 이벤트 핸들러 함수에서의 이벤트 아규먼트는 브라우저의 종류와 관계없이 이벤트를 처리할 수 있도록 SyntheticEvent<T> 타입으로 추상화

각 이벤트 아규먼트 타입에서 사용할 수 있는 속성 참고
https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/v17/index.d.ts


단방향 데이터 바인딩

리액트 애플리케이션은 상태(데이터)가 바뀌면 UI(화면)가 갱신되는 단방향 데이터 바인딩 구조
양방향 데이터 바인딩을 지원하지 않으므로 UI에서 입력한 값이 상태에 반영되지 않음
그래서 리액트는 UI에서 입력한 값을 상태에 반영하기 위해 리액트 이벤트 시스템을 이용한다.
이벤트 핸들러 함수에서 상태를 변경하면 변경된 상태가 UI를 갱신시킴



이벤트 핸들러와 상태 변경

세터 함수는 비동기로 작동하기 때문에 이벤트 핸들러 안에서 같은 상태를 여러 번 변경하면 문제가 발생할 수 있다.

리액트의 상태는 왜 비동기로 실행되도록 설계됐을까?
⇒ 리액트 애플리케이션의 렌더링 성능 때문
상태 변경과 렌더링 작업은 깊이 연관되어 있어 상태 변경을 동기적으로 실행하면 불필요한 렌더링이 추가로 발생하고, 이에 따라 렌더링 성능이 느려진다.
즉, 리액트에서의 상태 변경이 비동기 실행임을 고려해 하나의 이벤트 핸들러 함수에서는 같은 상태를 여러 번 변경하지 않는 것이 바람직 하다.


제어 컴포넌트와 비제어 컴포넌트

컴포넌트는 상태가 입력 필드를 제어하는지 아닌지에 따라 제어 컴포넌트(controlled component)와 비제어 컴포넌트(uncontrolled component)로 구분함

  • 제어 컴포넌트(controlled component)
    UI에서 입력 필드의 값이 상태(state)나 속성(props)에 의해 강하게 제어되는 컴포넌트
    상태, 속성이 바뀌지 않는 한 입력값을 변경할 수 없다.
    입력 필드의 값을 변경하려면 리액트의 이벤트 핸들러를 이용해 상태를 변경해야 함

  • 비제어 컴포넌트(uncontrolled component)
    입력 필드의 값이 상태나 속성에 의해 제어되지 않는 컴포넌트
    사용자가 쉽게 변경할 수 있지만 입력한 값이 상태에 반영되지 않는다.
    또한 사용자가 UI 입력 필드에 입력한 값을 알아내려면 브라우저의 HTML DOM에 직접 접근해야 하는 단점이 있음


제어 컴포넌트

제어 컴포넌트는 UI 요소의 값이 상태나 속성에 강하게 연결되어 있으므로 변경할 수 없다.
이 문제를 해결하려면 리액트 이벤트를 이용해 UI 입력 필드에서도 상태를 변경할 수 있도록 해야 한다.

...

const changeValue = (e: ChangeEvent<HTMLInputElement>) => {
  let newValue: number = parseInt(e.target.value);
  if (isNan(newValue)) newValue = 0;
  if (e.target.id === "x") setX(newValue);
  else setY(newValue);
};

...
X : <input id="x" type="text" value={x} onChange={changeValue} />
Y : <input id="x" type="text" value={y} onChange={changeValue} />

비제어 컴포넌트

비제어 컴포넌트는 상태, 속성에 의해 제어되지 않기 때문에 리액트 이벤트를 이용하지 않아도 사용자가 입력 필드의 값을 수정할 수 있다.
하지만 이때 수정한 값으로는 상태가 변경되지 않기 때문에 입력값을 획득하기 위해서 HTML DOM 요소에 직접 접근해야 한다.

그러나 리액트는 가상 DOM 기반으로 작동함
가상 DOM을 이용하지 않고 HTML DOM에 직접 접근하는 것은 비효율적이므로 꼭 필요한 경우이거나 성능 이슈가 발생할 가능성이 없는 컴포넌트에서만 사용할 것을 권장

비제어 컴포넌트는 비록 상태나 속성이 입력 필드를 제어하지는 않지만 초깃값을 부여할 수 있음
초깃값을 부여하는 속성은 모두 default~로 시작

...

const elemX = useRef<HTMLInputElement>(null);
const elemY = useRef<HTMLInputElement>(null);

...

const add = () => {
  let x1: number = parseInt(elemX.current ? elemX.current.value : "", 10);
  let y1: number = parseInt(elemY.current ? elemY.current.value : "", 10);
  ...
}
  
...

X : <input id="x" type="text" defaultValue={x} ref={elemX} />
Y : <input id="y" type="text" defaultValue={y} ref={elemY} />

비제어 컴포넌트에서 브라우저의 HTML DOM에 접근하기 위해 useRef() 리액트 훅을 이용함
HTML 요소 객체를 연결할 것이므로 초깃값을 null로 부여해도 되며, HTML 요소의 타입을 제네릭으로 지정해 ref객체를 리턴받음
사용자가 입력한 값을 획득하려면 ref 객체의 current 속성을 이용
current 속성값이 null일 수도 있으므로(HTML DOM이 연결되지 않은 경우) 삼항 연산을 이용해 current 속성이 있을 때만 문자열로 받아내도록 작성



상태 심화

세터 함수는 상태를 변경만 하는 것이 아니라 리액트 애플리케이션에 상태가 변경되었음을 알려서 컴포넌트가 다시 렌더링(re-render)되도록 함
한 컴포넌트가 다시 렌더링 되면 그 컴포넌트의 자식 컴포넌트들도 모두 다시 렌더링 됨
(자식 컴포넌트가 사용하는 데이터(예: 속성)에 변경된 것이 없다 하더라도)

가상 DOM은 브라우저 DOM에 업데이트하는 작업을 줄여주므로 렌더링 성능이 개선되지만, 이 과정에서 가상 DOM에 불필요한 쓰기를 반복하는 것도 분명히 성능에 나쁜 영향을 주기 때문에 개선해야 한다.
즉, 속성이나 상태에 변동이 없다면 가상 DOM에 대한 쓰기를 하지 않도록 해야 함


렌더링 최적화와 불변성

깊은 비교(deep compare)

  • 컴포넌트는 속성으로 전달받은 객체 트리를 따라 내려가며 이전 객체 트리와 상태나 속성이 다른 부분이 있는지를 일일이 비교하며 자기 자신을 다시 렌더링해야 할지를 결정

렌더링 성능을 최적화하기 위해 상태 트리에 대해 깊은 비교를 하면 오히려 렌더링 성능이 저하될 수 있음
이 문제점을 해결하는 방법은 상태 트리 끝단의 값이 변경되면 상태 트리의 루트 경로로 거슬러 올라가는 경로상의 객체를 모두 새로운 객체로 바꿔주는 것

그러면 기존 객체 트리와는 다른 새로운 객체 트리가 변경된 경로상에서 만들어진다.
이러한 작업은 immer와 같은 불변성 라이브러리가 담당함

얕은 비교(shallow compare)

  • 불변성을 가지도록 객체를 변경하면 제일 끝단의 상태를 변경하더라도 컴포넌트의 속성으로 전달된 하위 객체 트리와 이전의 객체 트리가 단순히 같은 객체인지를 비교하는 것만으로도 렌더링 여부를 쉽게 결정할 수 있음
    즉, 렌더링 최적화가 간단해짐

불변성 라이브러리 immer

npm install immer

복잡한 객체 트리가 불변성을 가지도록 별도의 불변성 라이브러리를 사용하는 경우가 많은데, 가장 많이 사용되는 것이 immer(immutability + ~er)이다.

// immer 함수를 produce라는 이름으로 임포트
import produce from 'immer'

const currentState = [
  { todo: "Learn es6", done: true },
  { todo: "Try immer", done: false }
]

// produce 함수의 첫 번째 인자: 변경 대상 객체
// produce 함수의 두 번째 인자: 불변성 변경 함수
// 상태 변경 함수의 인자: 상태 변경을 위한 draft 버전의 객체
const nextState = produce(currentState, (draft) => {
  draft[1].done = true
})

produce 함수의 첫 번째 인자는 기존 객체이며, 두 번째 인자는 불변성 변경 함수이다.
불변성 변경 함수의 인자인 draft 객체를 자유롭게 변경하여 불변성 변경 함수 실행이 완료되면 불변성을 가진 새로운 객체(nextState)가 리턴됨
immer를 사용해 객체 트리 끝단을 변경하면 루트로 거슬러 올라가는 경로상의 모든 객체를 새로운 객체로 만든다.

리액트 애플리케이션의 상태는 불변성을 가지도록 변경해야 함



컨테이너 컴포넌트와 표현 컴포넌트

컨테이너 컴포넌트(container component)

상태와 상태변경, 비즈니스 로직을 처리하는 연산 기능이 있으며 UI와 스타일 정보는 포함하지 않고 단순히 자식 컴포넌트를 조합하도록 작성

표현 컴포넌트(presentational component)

부모 컴포넌트로부터 속성(props)를 전달받아 UI를 렌더링하는 기능을 수행한다.
연산과 로직으로부터 UI를 분리해서 작성하므로 재사용성이 높음
자신의 상태를 가지지 않지만 수명 주기 관리가 필요하지 않은 상태라면 표현 컴포넌트 내부에 상태를 가질 수도 있음

컨테이너 컴포넌트에서의 상태 변경만을 추적하면그 하위 컴포넌트의 UI가 어떻게 바뀔지를 예측할 수 있다.
즉, 상태 변경 추적이 용이해지고 그에 따라 디버깅도 좀 더 쉽게 할 수 있다.

profile
알고 쓰자!

0개의 댓글