이전 포스팅에서는 먼저 프로젝트를 시작하기 전에 우선 파악해야 하는 정보를 중심으로 다뤘다. 이번 포스팅에서는 실제로 화면 구현을 하면서 새롭게 알게되는 것들을 작성하고자 한다.
JSX는 javascript의 문법 확장버전이다. 따라서 javascript와 연계해서 화면 구현을 하기가 매우 수월한데 즉, 동적으로 태그를 추가하기도 편하다는 뜻이다.
예를 들어서 bar의 기능을 가진 태그를 여러번 적용해야 한다면 이처럼 동적으로 생성한 후
// 길이 8 배열, {key: value}는 객체리터럴 생성 시, Array.from()은 두 번째 인수로 매핑 함수를 받을 수 있음.
const bars = Array.from({ length: 8 }, (_, index) => (
<div className="bar" key={index}></div>
));
jsx내에서 사용할 수 있다.
<div className="">
{bars} {/*bar*/}
</div>
react에서는 기본적으로 컴포넌트라는 기본 빌딩 블록을 사용하는 UI를 구성하는 기본단위이다. 컴포넌트를 통해 UI를 독립적이고 재사용 가능한 조각으로 나누어 관리하게 되는데 화면의 각 부분을 컴포넌트라고 보면 된다. 화면 하나도 하나의 컴포넌트이다.
두 가지 주요 컴포넌트 유형으로는
1. 함수형 컴포넌트 (Functional component): 단순히 UI를 렌더링 하는 함수로 상태 관리, 생명주기 메서드 사용 시 useState, userEffect같은 React hook을 사용한다.
2. 클래스형 컴포넌트 (Class Components): ES6 클래스를 기반으로 한 컴포넌트로 state와 생명주기 메서드를 내장하고 있다. (함수형 컴포넌트에서는 이 기능을 Hooks를 통해 구현한다.)
컴포넌트의 주 역할은 UI를 정의하고 데이터와 UI간의 상호작용을 관리한다. 말했듯이 컴포넌트의 조합으로 더 크고 복잡한 컴포넌트(화면 등)를 만들어서 관리한다.
🧀ES6(ECMAScript 6 - javascript의 표준 스펙을 정의한 규격)
컴포넌트 내에서 관리되는 동적인 데이터로 시간이 지남에 따라 변경될 수 있으며 state가 변경되면 react는 자동으로 해당 컴포넌트를 리-렌더링 한다. 주요 특징으로는
1. 동적 데이터: 사용자의 상호작용에 따라서 state가 변경되고 해당 state변경에 따라서 UI를 업데이트 할 수 있다. (hook, this.state)
2. 컴포넌트 내부에서 관리: state는 컴포넌트 자체에서 관리되며 컴포넌트 간 공유되지 않는다. 따라서 상태를 공유하고 싶다면 상위 컴포넌트에서 관리해야 한다.
3. setState(useState)로 업데이트: state사용 시 함수형 컴포넌트에서는 const [count, setCount] = useState(0) 이와 비슷한 형태로 사용하게 되는데 이 때 count가 변경되는 값이고 setCount가 count에 변경된 값을 전달하는 함수라고 보면 된다.
useState의 사용법
import React, { useState } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
};
export default Counter;
setState의 사용법
import React from 'react';
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
increment = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
return (
<div>
<p>{this.state.count}</p>
<button onClick={this.increment}>Increment</button>
</div>
);
}
}
export default Counter;
props는 properties의 약자이다. 즉, 전달되는 어떠한 데이터를 가진 객체라고 보면 되는데 부모 컴포넌트가 자식 컴포넌트에 데이터를 전달할 때 사용되며 read-only다. 따라서 props는 컴포넌트 내부에서 수정할 수 없다. 주요 특징으로는
props와 state는 둘다 데이터를 다루지만 명확한 차이점이 있다. 혹시나 헷갈리지 않도록 짚고 넘어가자
문법을 배우면서 && 연산자를 사용하는 형태를 보게 됐는데 &&를 조건문 검사로만 사용하는 것이 아니라 조건이 만족되었을 때 그 다음 함수를 실행 시킬 수도 있게 사용할 수도 있다 라는 것을 알았다. 예를 들어 아래와 같이 사용한다면 someFunction은 someCondition이 true이면 실행, false이면 실행되지 않는다. 즉, 왼쪽 조건이 참일때만 오른쪽 코드가 실행된다. (left -> right)
onPressIn={() => someCondition && someFunction(props)} // true인 경우 someFunction이 실행
이 문법은 kotlin에서는 이런 형식으로 볼 수 있다.
if (someCondition) {
someFunction(props)
}
마찬가지로 or 연산자 또한 왼쪽 조건이 거짓일 때만 오른쪽 코드를 실행 시킬 수 있다.
userInput || getDefaultValue(); //falsy인 경우 getDefaultValue()가 실행.
javascript에서는 이런 식으로 조건문을 간단하게 사용할 수 있다고 한다. 따라서 사용되는 연산자들을 정리하고자 한다. 기본적인 삼항 연산자나 조건문은 넘어간다.
null 또는 undefined인 경우에만 기본값을 설정하는 데 사용된다. ||과 유사하나 Falsy value 값은 기본값으로 간주하지 않는다.
const value = userInput ?? "default value";
간단히 살펴보자. 예를 들어 user라는 객체는 이처럼 생겼다. name 필드 하나만 가지고 있는 상태다.
const user = { name: "Alice" }; // name = Alice
여기에 name 필드에 Guest와 age라는 새로운 필드멤버를 만들어서 기본값을 준다.
const { name = "Guest", age = 18 } = user;
이러면 해당 user 객체는 변화되지 않고 기본값을 설정할 수 있다.
우선 알아두어야 하는 건 React에서 Hook을 호출할 때 절차지향적으로 동작하기 때문에 한 파일내에서 여러개의 Hook을 사용할 때 상호작용이 되어야 하는 경우 주의가 필요하다.
훅은 함수형 컴포넌트에서 React State와 생명주기 기능을 연동 할 수 있게 해주는 함수로 기존의 클래스형 컴포넌트에서 사용되었던 생명주기 연동 기능을 함수형 컴포넌트에서 할 수 있도록 개발된 간편한 api이다. class안에선 동작하지 않지만 class없이 React를 사용할 수 있게 해주는 것이다.
const [value, function] = hook;
Hook은 중첩된 함수나 반복, 조건문에서 호출하게 될 경우 순서가 엄청나게 꼬일 가능성이 있다. Hook은 생명주기와 State를 연동하는 만큼 호출되는 순서에 의존하기 때문에 최상위에서 부르는 것을 추천한다.
또한 언급했듯이 class 없이 사용하는 것이기 때문에 react 함수 내에서만 호출해야 한다. 일반적인 js함수에서는 안되고 함수형 컴포넌트나 custom hook에서 호출이 가능하다.
1. useState (state 관리): 가장 기본적인 훅으로 컴포넌트 안에서 상태 관리시 사용한다. 초기 상태 값을 지정해야 하며 값이 변경되는 경우 갱신할 수 있는 함수를 반환한다. 함수가 반환되면 React 컴포넌트가 리렌더링 되게 된다. 또한 useState Hook의 state는 객체일 필요가 없다. (물론 객체여도 된다.)
const [count, setCount] = useState(0);
2. useEffect (side effect 수행): React 컴포넌트가 렌더링 될 때 마다 특정 작업(side effect)을 수행시키도록 설정할 수 있는 훅으로 mount(생성)/unmount(제거)/update 시점 제어가 가능하며 의존성 배열을 통해 이 훅이 언제 실행될 지 제어할 수 있다. 외에도 비동기 작업, 타이머 설정, 외부데이터 호출 등 다양한 처리가 가능하다.
useEffect(() => {
document.title = `You clicked ${count} times`;
return () => {
// Cleanup code
};
}, [count]);
3. useContext: 컴포넌트 트리 전체에 데이터를 전역적으로 전달할 수 있는 context 중 특정 context를 가져오는 역할이다. 따라서 전역값을 쉽게 관리할 수 있다.
const value = useContext(MyContext);
4. useReducer: usestate랑 비슷하지만 더 복잡한 상태 로직을 사용할 때 사용된다. 현재 상태와 업데이트를 위해 필요한 정보를 담은 액션을 받아 새로운 상태를 반환하며 새로운 상태 생성시 불변성을 지켜주어야 한다.
const [state, dispatch] = useReducer(reducer, initialState);
5. useCallback (특정 함수 재사용): memoization(동일한 계산 반복 시 이전 계산 값을 저장하여 반복을 제거)된 callback을 반환하여 의존성 배열이 변경되지 않는다면 동일한 callback을 반환한다. 즉, 렌더링 될 때 마다 수행되는 함수형 컴포넌트 내부에서 발생하는 연산 최적화가 가능하다.
const Component = ({ a, b }) => {
// useCallback
const memoizedCallback = useCallback(() => {
console.log(a, b);
}, [a, b]);
return <ChildComponent onClick={memoizedCallback} />;
};
6. useMemo (연산한 값 재사용): userCallback과 거의 동일한 기능을 수행하지만 userCallback은 함수에 대해서 작동하고 userMemo는 값에 대해서 작동한다는 차이점이 있다.
const expensiveCalculation = (a, b) => {
console.log("Calculating...");
return a + b;
};
const Component = ({ a, b }) => {
// useMemo
const memoizedValue = useMemo(() => expensiveCalculation(a, b), [a, b]);
return <div>{memoizedValue}</div>;
};
7. useRef (DOM선택, 컴포넌트 안에서 조회/수정할 수 있는 변수 관리): DOM 요소나 상태 변경 시 리렌더링 없이 값을 유지하고 싶은 경우에 사용하며 변경 가능한 ref 객체를 반환한다. ref 객체의 current값은 실제 엘리먼트를 가르키게 된다.
const inputRef = useRef(null);
8. useImperativeHandle: 부모 컴포넌트가 자식 컴포넌트의 인스턴스를 직접 조작할 수 있도록 커스텀 ref를 노출시킨다.
보통 함수형 컴포넌트에서는 ref를 props로 전달받지 못하지만, ref를 전달받는 자식 컴포넌트에서 React.forwardRef를 사용하면 이를 받을 수 있다. forwardRef는 상위 컴포넌트에서 전달받은 ref를 하위 컴포넌트로 전달하는 역할을 한다. 이 함수는 React.ReactNode 타입을 반환하기에 일반 함수형 컴포넌트와 동일하게 JSX 문법을 사용하여 렌더링이 가능하다.
따라서 useImperativeHandle을 사용하면, 자식 컴포넌트 내에서 특정 동작이나 메서드를 부모 컴포넌트에 노출시켜, 부모 컴포넌트가 해당 동작을 직접 호출할 수 있게 된다. 이는 주로 DOM 요소나 자식 컴포넌트의 특정 기능에 직접 접근해야 할 때 사용된다. 예를 들어, 특정 입력 필드를 포커스하게 하거나, 애니메이션을 수동으로 트리거해야 하는 경우에 유용하다.
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
9. useLayoutEffect: useEffect와 유사하지만 DOM 업데이트 후 시각적 변화가 화면에 그려지기 전 동기적으로 실행되며 레이아웃 측정을 해야하는 경우 사용된다.
useLayoutEffect(() => {
const height = elementRef.current.clientHeight;
// Do something with height
}, []);
10. useDebugValue: 사용자 정의 훅에서 ReactDevTools에 표시할 디버그 정보 설정 시 사용한다.
useDebugValue(value ? 'Online' : 'Offline');
11. useDeferredValue: 입력 값이 변경되면, 그 값을 느리게 적용하여 사용자가 느끼는 렌더링 지연을 줄일 수 있다.
const deferredValue = useDeferredValue(inputValue);
12. useTransition: UI 업데이트와 관련된 전환 애니메이션의 시작 및 중지를 제어한다. 비동기적인 UI 상태를 만들 때 유용하다.
const [isPending, startTransition] = useTransition();
13. useTransition: 유한 ID를 생성하여 클라이언트와 서버 간의 HTML을 일치시키는 데 유용하다.
const id = useId();
14. useSyncExternalStore: 외부 스토어의 상태를 읽고 구독하게 된다.
const state = useSyncExternalStore(subscribe, getSnapshot);
15. useInsertionEffect: CSS-in-JS 라이브러리에서 스타일을 동기적으로 삽입하는 데 사용된다.
useInsertionEffect(() => {
// Insert style
}, []);
함수를 작성할 때 보면 function보다는 const를 많이 사용하는 것을 볼 수 있다. 적어도 내가 본 대부분의 함수는 const MyFunction = () => {} 과 같은 형식으로 작성되어 있었다. 왜 그럴까?
1. 불변성 보장: const는 상수값을 정의할 때 사용하는 키워드다. 변수는 마찬가지로 재할당이 불가능하지만 함수에 사용하게 되면 함수의 상태 즉, 함수가 가지는 값은 runtime시에 변경될 수 있다.
2. 호이스팅 이슈 방지: function 키워드로 정의된 함수는 호이스팅(hoisting)에 의해 정의 이전에도 호출할 수 있다. 하지만 const로 정의된 함수는 호이스팅이 적용되지 않아 정의된 이후에만 접근 가능하다.
3. 재정의 방지: const로 함수를 정의하면 상수를 선언한 것 처럼 다른 곳에서의 재할당이 불가능하다.
또 여러 이유가 있을 수 있겠지만 이러한 이유로 const를 함수 정의시 많이 사용한다.
🧀호이스팅(hoisting)? 인터프리터가 변수와 함수의 메모리 공간을 선언전에 미리 할당하는 것. 즉, 선언한 변수 및 함수들이 코드 실행 전 최상단으로 이동하는 것을 뜻한다.
우선 함수형 컴포넌트를 사용할 때 ()안에 들어가는 것은 기본적으로 props객체이다. 이 props객체는 해당 컴포넌트에 전달된 모든 속성을 포함하는 javascript객체이다. 이걸 꺼내쓰는 방식이 여러가지가 있는데 props객체 자체를 받아와서 적용을 하거나 아니면 바로 props객체에서 가져올 속성을 정의해서 쓸 수도 있다.
export const TestBackground = ({ children }) => (
<View>
{children}
</View>
);
위 문법을 보면 children객체를 받아서 view태그 안에 삽입하게 되어 있는데 이 코드를 실제로 사용할 때는 ()안에 props를 전달하는 게 아니라 그냥 태그 안에 작성만 해도 children으로 가져오게 된다. 여기서 children은 그냥 이름이 아니라 특별한 속성으로 컴포넌트 내부에서 자식 요소를 렌더링 할 때 사용하는 속성이다.
<TestBackground>
<View> // 여기서 부터 children
<Text/>
<Button/>
</View>
</TestBackground>
props를 직접 받을 수도 있다. 만약 함수형 컴포넌트에서 매개변수를 이렇게 props를 받는 형태로 작성한다면 (괄호는 있거나 없어도 상관없다.) 그냥 props객체의 속성에 접근해서 사용하면 된다.
const GradientBackground = (props) => {
return (
<LinearGradient>
{props.children} // props를 직접 받아서 children을 설정하는 형태
</LinearGradient>
);
};