0411 - 0417
0411
이펙티브 타입스크립트 스터디 회고
약 9주간의 함께한 스터디가 모두 마무리되었다.
생각보다 책의 내용이 쉽지 않았고, 예제를 모두 이해해가며 하려다보니 도중에 어려움이 많았지만 함께 스터디했기에 짧은 시간 내에 책을 마무리할 수 있었던 것 같다.
이번 스터디를 통해 얻은 것은 다음과 같다.
- 타입스크립트의 컨셉과 기본 개념에 대한 이해
- 시간을 정해놓고 일정 분량을 학습함으로써 얻은 학업 성취도
- 타입스크립트를 더 효과적으로 사용하기 위한 여러 기법들
하지만 1회독 만으로 책을 완전히 내재화시키지는 못한 것 같고, 앞으로 꾸준히 복습하며 내재화시켜야한다고 느끼고 있다.
이번 스터디를 잘 마무리하기까지 스터디장님의 역할이 컸다고 생각한다.
이번 스터디와 저번에 진행했던 프로젝트를 통해 ‘함께’의 즐거움과 매력을 알게 되었고, 나 역시 다른 사람들에게 좋은 영향을 미치는 사람으로 나아가고 싶다는 목표를 가지게 되었다.
경험을 충분히 했으니, 이제는 ‘어떻게’ 더 큰 시너지를 낼 것인지에 대한 고민을 할 필요가 있을 것 같다.
팀원들과의 회고
0412
useCallback, useMemo, React.memo
const Counter = ({ initialCount = 0, step = 1 }) => {
const [count, setCount] = useState(initialCount);
useEffect(() => {
count < 0 && setCount(0);
}, [count]);
const [renderingTester, setRenderingTester] = useState(0);
useEffect(() => {
setTimeout(() => setRenderingTester(renderingTester + 1), 3000);
}, [renderingTester]);
const increment = () => {
setCount(count + step);
};
const decrement = () => {
setCount(count - step);
};
return (
<CountContainer
count={count}
onIncrement={increment}
onDecrement={decrement}
/>
);
};
코드를 위와 같이 작성하면 불필요한 리렌더링이 발생한다.
함수 컴포넌트는 렌더링 될 때마다 몸체가 다시 실행되므로 컨텍스트를 기억하기 위해서는 훅을 사용한다.
그리고 상위 컴포넌트는 기억된 상태 또는 업데이트 함수를 하위 컴포넌트에 전달한다.
상태는 훅에 의해 기억되어 관리되니 문제되지 않지만, 함수 내부에 선언된 함수는 매번 렌더링 될 때마다 다시 정의되므로 문제가 된다.
정확히 말하면 props로 전달되는 함수인 경우, 자식 컴포넌트에서 props가 변경되었기에 자식 컴포넌트까지 불필요하게 리렌더링이 발생한다.
문제 해결 방법
하위 컴포넌트에 전달되는 함수를 기억해두면, 컴포넌트 업데이트 시 다시 렌더링 될 때마다 기억된 함수를 사용해 문제를 해결할 수 있다.
전달될 함수를 기억하려면 다음의 훅 중 하나를 사용한다.
useCallback()
기억해야 할 데이터 타입이 함수인 경우, useCallback()
훅을 사용한다. 이는 메모이제이션된 콜백을 반환한다.
이 훅은 useEffect()
훅과 마찬가지로 종속성 배열을 통해 조건에 따라 기억 여부를 재설정할 수 있다.
이는 불필요한 렌더링을 방지하기 위해 자식 컴포넌트에 콜백을 전달할 때 유용한다.
콜백 안에서 참조되는 모든 값은 의존성 값의 배열에 나타나야 한다.
const increment = useCallback(() => {
setCount(count + step);
}, [count, step]);
const decrement = useCallback(() => {
setCount(count - step);
}, [count
useMemo()
자바스크립트의 데이터 타입을 기억해야 할 때 사용한다. 기억해야 할 타입이 함수인 경우 useCallback()
에 비해 코드 가독성이 현저히 떨어지므로, 함수를 기억해야 할 경우는 useCallback()
을 사용하자.
배열이나 객체를 하위 컴포넌트로 전달하는 경우 불필요한 렌더링을 방지하기 위해 사용하면 유용하다.
또한, 이 최적화는 모든 렌더링 시의 고비용 계산을 방지하게 해 준다. 같은 값을 얻는 데 동기적으로 오래 걸리는 함수를 반복해서 실행하지 않고, 메모된 이전 값을 그대로 사용하여 좀 더 성능을 개선할 수 있다.
useMemo로 전달된 함수는 렌더링 중 실행되므로, 렌더링 중에서 하지 않는 것은 이 함수 내에서 하면 안 된다.
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
React.memo
하지만 위와 같이 같은 함수를 전달해도 불필요한 리렌더링 문제는 해결되지 않는다.
그 이유는 상위 컴포넌트가 다시 렌더링 될 때, 하위 컴포넌트 또한 다시 렌더링 되기 때문이다.
해당 문제를 해결하려면 컴포넌트도 기억하고 있어야 한다. 상위 컴포넌트가 다시 렌더링 되어도, 하위 컴포넌트에 전달한 props는 동일하고, 이전 컴포넌트가 기억되고 있으니 불필요한 렌더링 문제를 막을 수 있다.
컴포넌트를 기억하려면 memo와 useMemo 훅을 사용할 수 있다.
React.useMemo() 훅을 사용할 경우 함수 컴포넌트 내부에서 기억해야 할 값을 훅으로 감싼 후 반환합니다. 컴포넌트는 다시 렌더링 되겠지만, 기억된 값이 반환되므로 성능 저하 문제를 해결할 수 있습니다.
const CountContainer = ({ count, onIncrement, onDecrement, ...restProps }) => {
return useMemo(
() => (
<Container {...restProps}>
<CountButton
label="카운트 감소"
size={40}
flex="0 0 40"
disabled={count <= 0}
onUpdate={onDecrement}
>
-
</CountButton>
<CountDisplay count={count} />
<CountButton
label="카운트 증가"
size={40}
flex="0 0 40"
onUpdate={onIncrement}
>
+
</CountButton>
</Container>
),
[count, onIncrement, onDecrement, restProps]
);
};
React.memo는 고차 컴포넌트이다.
React.memo를 사용하면 컴포넌트의 props가 변경되지 않는 이상 기억된 컴포넌트를 사용하게 된다.
호출하고 결과를 메모제이션하도록 래핑하여 경우에 따라 성능 향상을 누릴 수 있다.
즉, 컴포넌트가 필요한 경우에만 렌더링하게 되고, 그 외에는 렌더링 자체를 발생시키지 않는다.
이 메서드는 오직 성능 최적화를 위해서 사용해야 한다. 렌더링을 방지하기 위해서 사용하면 안 된다.
그러나 대부분의 경우 불필요한 렌더링 최적화에 너무 많은 시간을 쏟아서는 안 된다. 리액트는 매우 빠르며 최적화보다 더 나은 시간을 할애할 수 있도록 생각할 수 있는 일이 너무 많다.
결론적으로 최적화에는 비용이 따르며 해당 비용 관련 이점을 알아야 하고, 최적화를 하기 이전에 측정을 해 봐야 한다.
0413
이벤트
주의사항
- 이벤트 이름은 카멜 표기법으로 작성
- 이벤트에 실행할 자바스크립트 코드를 전달하는 것이 아니라, 함수 형태의 값 전달
- DOM 요소에만 이벤트 설정 가능. 직접 만든 컴포넌트에 onClick 값을 설정한다면 그냥 이름이 onClick인 props를 전달해 줄 뿐이다.
리액트의 이벤트
onChange={(e) => {
console.log(e.target.value);
}}
이와 같이 e 객체를 전달받아 사용할 수 있는데, SyntheticEvent로 웹 브라우저의 네이티브 이벤트를 감싸는 객체이다.
네이티브 이벤트와 인터페이스가 같으므로 순수 JS에서 HTML 이벤트를 다룰 때와 똑같이 사용하면 된다.
다만 네이티브 이벤트와 달리 이벤트가 끝나고 나면 초기화되므로 정보를 참조할 수 없다.
만약 비동기적으로 이벤트 객체를 참조할 일이 있다면 e.persist()
함수를 호출해 주어야 한다.
class 컴포넌트 메서드 this 바인딩
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleClick = this.handleClick.bind(this);
}
handleChange(e) {
this.setState({
message: e.target.value,
});
}
handleClick() {
alert(this.state.message);
this.setState({ message: '' });
}
클래스의 임의 메서드가 특정 HTML 요소의 이벤트로 등록되는 과정에서 메서드와 this의 관계가 끊어져 버린다.
이 때문에 임의 메서드가 이벤트로 등록되어도 this를 컴포넌트 자신으로 제대로 가리키기 위해서는 메서드를 this와 바인딩하는 작업이 필요하다.
클래스 필드 문법을 사용하면 화살표 함수 형태로 메서드를 정의하여 좀 더 간단하게 코드를 작성할 수 있다.
handleChange = (e) => {
this.setState({
message: e.target.value,
});
};
handleClick = () => {
alert(this.state.message);
this.setState({ message: "" });
};
ref
ref를 사용하는 경우
- 포커스, 텍스트 선택영역, 혹은 미디어의 재생을 관리할 때
- 애니메이션을 직접적으로 실행시킬 때
- 서드 파티 DOM 라이브러리를 React와 같이 사용할 때
- 위와 같이 DOM을 꼭 직접적으로 건드려야 할 때
콜백 ref
ref={(ref) => {
this.input = ref;
}}
ref를 만드는 가장 기본적인 방법은 콜백 함수를 사용하는 것이다.
ref를 달고자 하는 요소에 ref라는 콜백 함수를 props로 전달해 주면 된다.
이 콜백함수는 ref 값을 파라미터로 전달받는다. 그리고 함수 내부에서 파라미터로 전달받은 ref를 컴포넌트의 멤버 변수로 설정해 준다.
이렇게 하면 this.input은 input 요소의 DOM을 가리킨다.
createRef
class ValidationSample extends Component {
input = React.createRef();
render() {
return (
<div>
<input ref={this.input} />
</div>
);
}
}
ref를 만드는 또 다른 방법은 createRef 함수를 사용하는 것이다.
이 함수를 사용해서 만들면 더 적은 코드로 쉽게 사용할 수 있다.
우선 컴포넌트 내부에서 멤버 변수로 React.createRef()
를 담아주어야 한다.
그리고 해당 멤버 변수를 ref를 달고자 하는 요소에 ref props로 넣어 주면 설정이 완료된다.
설정한 뒤 ref를 설정해 준 DOM에 접근하려면 this.input.current를 조회하면 된다.
콜백 함수를 사용할 때와 다른 점은 .current
를 넣어 주어야 한다는 것이다.
컴포넌트에 ref 달기
<MyComponent
ref={(ref) => {
this.myComponent = ref;
}}
/>
- 리액트에서는 컴포넌트에도 ref를 달 수 있다. 이 방법은 주로 컴포넌트 내부에 있는 DOM을 컴포넌트 외부에서 사용할 때 쓴다.
- 이렇게 하면 Mycomponent 내부의 메서드 및 멤버 변수에도 접근할 수 있다. 즉, 내부의 ref에도 접근할 수 있다.
- ref 어트리뷰트가 커스텀 클래스 컴포넌트에 쓰였다면, ref 객체는 마운트된 컴포넌트의 인스턴스를 current 프로퍼티의 값으로서 받는다.
- 함수 컴포넌트는 인스턴스가 없기 때문에 함수 컴포넌트에 ref 어트리뷰트를 사용할 수 없다.
컴포넌트에 ref 달고 내부 메서드 사용
<div>
<ScrollBox ref={(ref) => (this.scrollBox = ref)} />
<button onClick={() => this.scrollBox.scrollToBottom()}>맨 밑으로</button>
</div>
- 문법상으로는
onClick={this.scrollBox.scrollToBottom}
같은 형식으로 작성해도 틀린 것은 아니지만 처음 렌더링될 때는this.scrollBox
값이undefined
이므로 오류가 발생한다. - 화살표 함수 문법을 사용하여 아예 새로운 함수를 만들고 그 내부에서 실행하면, 버튼을 누를 때 값을 읽어와서 오류가 발생하지 않는다.
주의점
ref를 컴포넌트끼리 데이터를 교류할 때 사용해서는 안 된다.
이는 잘못된 것으로 리액트 사상에 어긋난 설계이다.
컴포넌트끼리 데이터를 교류할 때는 언제나 부모, 자식 흐름으로 교류해야 한다.
0414
라이프사이클 메서드
모든 리액트 컴포넌트에는 라이프사이클이 존재한다.
컴포넌트 수명은 페이지에 렌더링되기 전인 준비 과정에서 시작하여 페이지에서 사라질 때 끝난다.
라이프사이클 메서드를 이용해 컴포넌트 초기 렌더링 시, 작업을 처리하거나 컴포넌트를 업데이트하기 전후로 어떤 작업을 처리하거나, 불필요한 업데이트를 방지할 수 있다.
라이프사이클 메서드는 클래스 컴포넌트에서만 사용할 수 있다.
라이프사이클은 총 세 가지, 즉 마운트, 업데이트, 언마운트 카테고리로 나눈다.
DOM이 생성되고 웹 브라우저상에 나타나는 것을 마운트라고 한다.
render() 함수
render() { ... }
- 라이프사이클 메서드 중 유일한 필수 메서드이다.
- 이 메서드 안에서 this.props와 this.state에 접근할 수 있으며, 리액트 요소를 반환한다.
- 아무것도 보여 주고 싶지 않다면 null 값이나 false 값을 반환하도록 한다.
- 이 메서드 안에서 이벤트 설정이 아닌 곳에서 setState를 사용하면 안 되며, 브라우저의 DOM에 접근해서도 안 된다.
constructor 메서드
constructor(props) { ... }
- 이것은 컴포넌트의 생성자 메서드로 컴포넌트를 만들 때 처음으로 실행된다.
- 이 메서드에서는 초기 state를 정할 수 있다.
getDerivedStateFromProps 메서드
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.value !== prevState.value) {
return { value: nextProps.value };
}
return null; // state를 변경할 필요가 없다면 null 반환
}
- props로 받아 온 값을 state에 동기화시키는 용도로 사용하며, 컴포넌트가 마운트될 때와 업데이트될 때 호출된다.
componentDidMount 메서드
componentDidMount() {}
- 컴포넌트를 만들고, 첫 렌더링을 다 마친 후 실행한다.
- 이 안에서 다른 자바스크립트 라이브러리 또는 프레임워크의 함수를 호출하거나 이벤트 등록, setTimeout, setInterval, 네트워크 요청 같은 비동기 작업을 처리하면 된다.
shouldComponentUpdate 메서드
shouldComponentUpdate(nextProps, nextState) {}
- 이것은 props 또는 state를 변경했을 때, 리렌더링을 시작할지 여부를 지정하는 메서드이다.
- 메서드 안에서 반드시 true나 false를 반환해야 하고, 기본값은 true이다.
- 이 메서드가 false를 반환하면 업데이트 과정은 종료된다.
- 프로젝트 성능을 최적화할 때, 이 메서드를 사용할 수 있다.
getSnapshotBeforeUpdate 메서드
getSnapshotBeforeUpdate(prevProps, prevState) {
if (prevState.array !== this.state.array) {
const { scrollTop, scrollHeight } = this.list;
return { scrollTop, scrollHeight };
}
}
- render에서 만들어진 결과물이 브라우저에 실제로 반영되기 직전에 호출된다.
- 이 메서드에서 반환하는 값은 componentDidUpdate에서 세 번째 파라미터인 snapshot 값으로 전달받을 수 있는데, 주로 업데이트하기 직전의 값을 참고할 일이 있을 때 활용한다.
- UI 업데이트 반영이 매끄럽지 않은 경우 사용한다. ex) 스크롤바 위치 유지
componentDidUpdate 메서드
componentDidUpdate(prevProps, prevState, snanshot) {}
- prevProps 또는 prevState를 사용하여 컴포넌트가 이전에 가졌던 데이터에 접근할 수 있다.
- getSnapshotBeforeUpdate에서 반환한 값이 있다면 여기서 snapshot 값을 전달받을 수 있다.
componentWillUnmount 메서드
componentWillUnmount() {}
- 컴포넌트를 DOM에서 제거할 때 실행한다.
- componentDidMount에서 등록한 이벤트, 타이머, 직접 생성한 DOM이 있다면 여기서 제거 작업을 해야 한다.
componentDidCatch 메서드
componentDidCatch(error, info) {
this.setState({
error: true,
});
console.log({ error, info });
}
- 컴포넌트 렌더링 도중 에러가 발생했을 때 애플리케이션이 먹통이 되지 않고 오류 UI를 보여줄 수 있게 한다.
- error는 파라미터에 어떤 에러가 발생했는지 알려 주며, info 파라미터는 어디에 있는 코드에서 오류가 발생했는지에 대한 정보를 준다.
- 이 메서드를 사용할 때는 컴포넌트 자신에게 발생하는 에러를 잡아낼 수 없고, 자신의 this.props.children으로 전달되는 컴포넌트에서 발생하는 에러만 잡아낼 수 있다.
- 그러나 공식문서에 대체 UI 렌더링 제어를 하려면
static getDerivedStateFromError()
를 대신 사용하고, componentDidCatch는 오류 로그 기록 등을 위해 사용하면 된다고 한다.
ErrorBoundary 예시
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, info) {
logComponentStackToMyService(info.componentStack);
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
export default ErrorBoundary;