2.3. Button 컴포넌트와 props
2.3-4. 계산기 버튼 만들기
이전 포스팅에 이어 작성한다.
2.3(Button 컴포넌트와 props) / 3-4
[React Native] #2-2. 계산기 인터페이스 만들기
25.02.10 이전 포스팅에서는 기본적인 리엑트 네이티브의 프로젝트 초기 설정을 진행했고, props의 객체 구조 분해할당에 대해 언급하고 마무리했다. 잠깐 다시 복습해 보자! [React Native] #2-1. 계산
udangtangtang-cording-oldcast1e.tistory.com
계산기를 구현하기 위한 버튼은 다음과 같다 : 정사각형 숫자 / 가로 형 숫자 / 정사각형 연산 / 세로형 연산
중복된 기능을 불필요하게 반복 생성하는 것이 아닌 4가지 종류의 버턴을 모두 활용할 수 있도록 컴포넌트를 수정하자.
색 선택하기
버튼을 디자인할 때 가장 중요한 요소 중 하나는 배경색이다. 적절한 색상을 선택하면 사용자의 경험을 향상하고, UI의 일관성을 유지할 수 있다. 색상을 결정할 때 디자이너의 도움을 받을 수 있다면 가장 좋지만, 그렇지 않은 경우 구글 머티리얼 디자인 색상표나 Tailwind CSS 색상표를 참고하는 것이 좋은 방법이다.
🔗 구글 머티리얼 디자인 색상표: Google Material Colors
🔗 Tailwind CSS 색상표: Tailwind CSS Colors
1️⃣ 색상 밝기와 상태 표현 📊
색상표에서 제공하는 숫자는 해당 색의 밝기를 의미한다.
✅ 낮은 숫자(100, 200, 300) → 밝은 색
✅ 중간 숫자(500) → 중심 색
✅ 높은 숫자(700, 800, 900) → 어두운 색
일반적으로 색상은 기본 상태, 활성화 상태, 비활성화 상태로 구분할 수 있으며, 이 상태 변화에 따라 색상을 다르게 설정해야 한다.
예를 들어, 버튼을 눌렀을 때 색상이 어두워지도록 만들고 싶다면,
- 기본 색을 500으로 설정하면 → 600, 700, 800, 900까지 4가지 선택지가 생긴다.
- 기본 색을 800으로 설정하면 → 900 한 가지 선택지만 가능하다.
이처럼 기본 색의 밝기를 무엇으로 설정하는지에 따라, 색상 변화를 줄 수 있는 범위가 달라진다. 따라서 디자인을 할 때는 이러한 요소를 고려해야 한다.
2️⃣ 색상 조합 방법 🎨
색상의 변화를 줄 때 두 가지 방법이 있다.
1. 같은 색에서 밝기 차이를 이용하는 방법 : 터치 시 같은 색상을 어둡게 변경 (예: 기본 색 500 → 터치 시 700)
2. 다른 색상을 조합하는 방법 : 버튼의 역할에 따라 서로 다른 색상을 적용
- 예: 긍정적인 버튼은 녹색, 부정적인 버튼은 빨간색
- 같은 밝기 값(예: 녹색 300, 빨간색 300)을 사용하면 UI가 자연스러움
Tip:
같은 밝기의 색을 사용하면 디자인이 어색하지 않고, 깔끔하게 정리되는 효과가 있다.
3️⃣ 실제 적용할 색상 선정 🖌️
이제 버튼 디자인을 위한 색상을 선택해 보자.
우리가 필요한 색상은 숫자 버튼의 배경색과 연산 버튼의 배경색이며, 각 버튼이 눌렸을 때의 색상까지 포함해 총 4가지 색상이 필요하다.
이 책에서는 Tailwind CSS 색상표를 기준으로 다음 색상을 사용한다.
- 숫자 버튼 (Zinc 계열) : 기본: Zinc 500 / 눌렸을 때: Zinc 700
- 연산 버튼 (Amber 계열) : 기본: Amber 500 / 눌렸을 때: Amber 700
숫자버튼
✅ 기존 코드
- StyleSheet.create()를 활용하여 styles.button, styles.text 등의 스타일을 적용함
- 터치 시 pressedButton 스타일을 추가적으로 적용하여 스타일 변경
- activeOpacity를 사용하여 터치 효과를 부드럽게 구현
- defaultProps를 사용하여 기본값을 설정함
✅ 새로운 코드
- styles.button을 기본으로 적용하면서, pressed && { backgroundColor: '#3 f3 f46' }을 사용해 직접 인라인 스타일 변경
- buttonStyle을 추가하여 부모 컴포넌트에서 외부 스타일을 동적으로 주입 가능
- onPress만을 사용하여 기본적인 터치 이벤트만 처리 / 터치했을 때의 스타일을 동적으로 설정
- PropTypes에서 isRequired를 설정하여 필수 props가 누락되지 않도록 처리
- buttonStyle을 PropTypes.object로 지정하여 선택적으로 사용할 수 있도록 개선
/Users/apple/React/RN/rn-calc/components/Button.js
import { Pressable, StyleSheet, Text } from 'react-native';
import PropTypes from 'prop-types';
const Button = ({ title, onPress, buttonStyle }) => {
return (
<Pressable
style={({ pressed }) => [
styles.button,
pressed && { backgroundColor: '#3f3f46' },
buttonStyle, // 전달받은 buttonStyle 적용
]}
onPress={onPress} // onPress 이벤트 추가
>
<Text style={styles.title}>{title}</Text>
</Pressable>
);
};
// PropTypes 정의 (올바른 문법 적용)
Button.propTypes = {
title: PropTypes.string.isRequired, // title은 필수 문자열
onPress: PropTypes.func.isRequired, // onPress는 필수 함수
buttonStyle: PropTypes.object, // buttonStyle은 선택적 객체
};
// 스타일 정의 (올바른 문법 적용)
const styles = StyleSheet.create({
button: {
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#71717a',
padding: 15, // 버튼 크기 조정
borderRadius: 8, // 버튼 둥근 모서리 적용
},
title: {
color: '#ffffff',
fontSize: 18, // 일반적인 버튼 크기에 맞게 조정
fontWeight: 'bold',
},
});
export default Button;
☑️ 새로운 코드
/Users/apple/React/RN/rn-calc/src/App.js
새로운 코드에서는 buttonStyle을 props로 전달하여, 버튼 크기를 유동적으로 조정할 수 있도록 수정했다.
버튼의 크기를 개별적으로 조정할 수 있도록 개선했으며 기존에는 styles 객체 내부에서 고정된 버튼 스타일을 적용해야 했지만, 이제는 buttonStyle을 통해 부모 컴포넌트에서 버튼 크기를 직접 제어 가능하다.
타일 관련 부분은 buttonStyle을 활용하여 개별적으로 조정할 수 있게 되면서, 스타일 객체를 최소화하고 컴포넌트의 재사용성을 높였고 버튼 크기 및 스타일을 외부에서 동적으로 설정할 수 있기 때문에 추가적인 UI 변경이 필요할 때, App.js의 수정 없이 부모 컴포넌트에서 쉽게 조정할 수 있는 유연성이 증가했다.
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View } from 'react-native';
import Button from '../components/Button';
// import Button from '../components/Button_ Pressable';
const App = () => {
const isError = true;
return (
<View style={styles.container}>
<StatusBar style="auto" />
<Text style={styles.text}>Calculate App</Text>
{/* <Text style={[styles.error, styles.text]}>StyleSheet</Text> */}
{/* <Text style={[styles.text, isError && styles.error]}>error</Text> */}
<Button
title="1"
onPress={() => console.log(1)}
buttonStyle={{ width: 100, height: 100 }}
></Button>
<Button
title="0"
onPress={() => console.log(0)}
buttonStyle={{ width: 200, height: 100 }}
/>
</View>
);
};
// 스타일 객체
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
text: {
fontSize: 30,
fontWeight: '700',
color: 'green',
},
button: {
padding: 15,
borderRadius: 5,
alignItems: 'center',
justifyContent: 'center',
},
buttonText: {
color: 'white',
fontSize: 20,
},
});
export default App;
![](https://blog.kakaocdn.net/dn/opbqL/btsMeSUFQYL/KRbpfBlH2rdYk1c1lYXcMk/img.jpg)
연산 버튼
Button 컴포넌트에서 버튼의 타입을 전달받고 그 타입에 따라 배경색이 달라지도록 수정해 보자.
기존 버튼 컴포넌트는 모든 버튼이 같은 스타일을 적용받도록 설계되어 있었다. 하지만 연산 버튼과 숫자 버튼은 시각적으로 명확하게 구분되어야 한다. 이를 해결하기 위해 ButtonTypes 객체를 정의하고, 버튼의 buttonType을 NUMBER 또는 OPERATOR로 구분하는 방식을 적용했다.
buttonType이 NUMBER인 경우에는 기본적인 회색 계열 색상을 사용하고, OPERATOR인 경우에는 주황색 계열을 적용하여 연산 버튼임을 직관적으로 인식할 수 있도록 했다. 또한, 터치했을 때 색상이 변경되도록 pressed 상태에 따라 동적 스타일을 적용했다.
발생한 문제와 해결 방법
연산 버튼을 추가하면서 몇 가지 문제점이 발생했다.
첫 번째 문제는 defaultProps를 사용한 기본값 설정이 React의 최신 버전에서는 지원이 중단될 예정이라는 경고 메시지였다. 이를 해결하기 위해 defaultProps를 제거하고 함수 매개변수의 기본값을 활용하는 방식으로 변경했다.
두 번째 문제는 버튼 스타일을 동적으로 변경하는 과정에서 buttonType이 제대로 전달되지 않는 경우가 발생했다. 원인은 Button 컴포넌트의 buttonType 프로퍼티를 정의할 때 잘못된 변수명을 사용했기 때문이었다. 이를 수정하여 buttonType={ButtonTypes.OPERATOR} 또는 buttonType={ButtonTypes.NUMBER}와 같이 명확히 전달할 수 있도록 개선했다.
마지막으로, PropTypes에서 buttonType의 유효성을 검사할 때, ButtonTypes 객체를 Object.values(ButtonTypes)로 변환하여 oneOf 옵션에서 올바르게 인식하도록 수정했다.
📌 Button.js (버튼 컴포넌트)
import { Pressable, Text } from 'react-native';
import PropTypes from 'prop-types';
// 버튼 타입 정의
const ButtonTypes = {
NUMBER: 'NUMBER',
OPERATOR: 'OPERATOR',
};
const Button = ({ title, onPress, buttonStyle = {}, buttonType = ButtonTypes.NUMBER }) => {
return (
<Pressable
style={({ pressed }) => [
{
backgroundColor: buttonType === ButtonTypes.NUMBER ? '#71717a' : '#f59e0b',
justifyContent: 'center',
alignItems: 'center',
padding: 15,
borderRadius: 8,
},
pressed && {
backgroundColor: buttonType === ButtonTypes.NUMBER ? '#3f3f46' : '#b45309',
},
buttonStyle, // 전달받은 스타일 적용
]}
onPress={onPress}
>
<Text style={{ color: 'white', fontSize: 18, fontWeight: 'bold' }}>{title}</Text>
</Pressable>
);
};
// PropTypes 정의
Button.propTypes = {
title: PropTypes.string.isRequired, // 버튼 텍스트 (필수)
onPress: PropTypes.func.isRequired, // 클릭 이벤트 (필수)
buttonStyle: PropTypes.object, // 스타일 객체 (선택)
buttonType: PropTypes.oneOf(Object.values(ButtonTypes)), // 버튼 타입 (NUMBER 또는 OPERATOR)
};
export { ButtonTypes };
export default Button;
📌 App.js (버튼 적용 예제)
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View } from 'react-native';
import Button, { ButtonTypes } from '../components/Button';
const App = () => {
return (
<View style={styles.container}>
<StatusBar style="auto" />
<Text style={styles.text}>Calculator</Text>
{/* 숫자 버튼 */}
<Button
title="0"
onPress={() => console.log(0)}
buttonStyle={{ width: 200, height: 100 }}
buttonType={ButtonTypes.NUMBER}
/>
{/* 연산자 버튼 */}
<Button
title="+"
onPress={() => console.log('+')}
buttonStyle={{ width: 100, height: 200 }}
buttonType={ButtonTypes.OPERATOR}
/>
<Button
title="-"
onPress={() => console.log('-')}
buttonStyle={{ width: 100, height: 100 }}
buttonType={ButtonTypes.OPERATOR}
/>
</View>
);
};
// 스타일 정의
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
text: {
fontSize: 30,
fontWeight: '700',
color: 'green',
},
});
export default App;
![]() |
![]() |
2.4 화면에 결과 출력하기
2.4-1. 더하기 빼기 화면 만들기
2.4(화면에 결과 출력하기) / 2.4-1
App 컴포넌트에 2개의 버튼을 만들고 각각 1씩 더하고 빼는 함수를 구현해 보자.
이를 위해 +버튼과 - 버튼을 생성하고, 해당 버튼을 클릭하여 더하고 뺀 값을 화면에 출력하도록 설정해야 한다.
아래 코드를 이용하여 두 버튼 사이의 간격을 추가한다.
아래 코드를 보면 paddingVertical: 10을 적용하여 + 버튼과 - 버튼 사이의 간격을 조정한 것을 확인할 수 있다.
<View style={{ paddingVertical: 10 }}></View>
paddingVertical은 위(Top)와 아래(Bottom)에 동일한 패딩을 적용하는 속성이다. 즉, paddingVertical: 10을 설정하면 위쪽과 아래쪽에 각각 10픽셀만큼의 패딩이 적용되어 버튼 간격이 조정된다.
React Native의 padding 관련 속성들은 다음과 같이 정리할 수 있다.
- padding: 모든 방향(위, 아래, 왼쪽, 오른쪽)에 동일한 패딩 적용
- paddingHorizontal: 왼쪽(left)과 오른쪽(right)에 동일한 패딩 적용
- paddingVertical: 위쪽(top)과 아래쪽(bottom)에 동일한 패딩 적용
이때, paddingVertical은 해당 요소 자체에 적용되는 스타일이므로, 빈 <View> 요소를 활용해야 한다. 직접 버튼에 paddingVertical을 적용하면 버튼의 높이가 늘어나므로 의도와 다르게 보일 수 있다.
또한 marginVertical은 외부 요소와의 거리를 조정하는 것이고, paddingVertical은 내부 요소와의 거리를 조정하는 개념이다.
만약 버튼 간격을 조절하는 것이라면 marginVertical을 사용하는 것도 가능하다.
import { StatusBar } from 'expo-status-bar'; // Expo의 상태 바(Status Bar) 컴포넌트 가져오기
import { StyleSheet, Text, View } from 'react-native'; // React Native의 주요 컴포넌트 가져오기
import Button, { ButtonTypes } from '../components/Button'; // 커스텀 버튼 컴포넌트와 버튼 타입 가져오기
const App = () => {
let result = 0; // 카운터 값을 저장하는 변수 (하지만 React의 상태 관리가 필요함)
return (
<View style={styles.container}> {/* 화면을 감싸는 컨테이너 */}
<StatusBar style="auto" /> {/* 상태 바 스타일을 자동으로 설정 */}
<Text style={styles.text}>{result}</Text> {/* 현재 카운트 값을 화면에 표시 */}
{/* 증가 버튼 */}
<Button
title="+" // 버튼의 텍스트 설정
onPress={() => {
result = result + 1; // 변수 값을 1 증가 (하지만 상태가 업데이트되지 않음)
console.log('+ : ', result); // 콘솔에 현재 값 출력
}}
buttonStyle={styles.button} // 버튼의 스타일 지정
buttonType={ButtonTypes.OPERATOR} // 버튼 타입을 OPERATOR로 지정
/>
<View style={{ paddingVertical: 10 }}></View> {/* 버튼 간 간격 조정 */}
{/* 감소 버튼 */}
<Button
title="-" // 버튼의 텍스트 설정
onPress={() => {
result = result - 1; // 변수 값을 1 감소 (하지만 상태가 업데이트되지 않음)
console.log('- : ', result); // 콘솔에 현재 값 출력
}}
buttonStyle={styles.button} // 버튼의 스타일 지정
buttonType={ButtonTypes.OPERATOR} // 버튼 타입을 OPERATOR로 지정
/>
</View>
);
};
// 스타일 정의
const styles = StyleSheet.create({
container: {
flex: 1, // 화면을 가득 채우도록 설정
backgroundColor: '#fff', // 배경색을 흰색으로 설정
alignItems: 'center', // 가로 방향 중앙 정렬
justifyContent: 'center', // 세로 방향 중앙 정렬
},
text: {
fontSize: 60, // 텍스트 크기 설정
fontWeight: '700', // 폰트 굵기 설정
// color: 'green', // 주석 처리된 텍스트 색상 설정
},
button: {
width: 100, // 버튼 너비 설정
height: 100, // 버튼 높이 설정
},
});
export default App; // App 컴포넌트 내보내기
![]() |
![]() |
하지만 터미널의 log에서는 숫자는 늘어나지만, 앱 화면상의 텍스트는 변경되지 않는다.
이는 자바스크립트 변수 result가 변경되어도 화면에 반영되지 않는다는 것이고 기존 렌더링 되었던 모습 그대로 유지하고 있음을 의미한다.
값이 변하면 변경된 값을 사용해 화면을 다시 렌더링 해야 한다.
이렇게 화면을 다시 렌더링 하는 것을 리렌더링이라고 한다.
2.4-2. 상태와 리렌더링
React Native에서 UI를 동적으로 업데이트하려면 상태(State)를 올바르게 관리해야 한다. 우리가 이전에 사용했던 일반 변수(result)는 값이 변경되더라도 화면에 자동으로 반영되지 않았다. 이는 React가 단순한 변수 변경을 감지하지 않기 때문이다.
이제 상태 관리의 개념과, 상태를 활용한 리렌더링에 대해 자세히 알아보자.
상태(State)란?
상태(State)는 컴포넌트 내부에서 관리하는 동적인 데이터이다. 일반적인 변수와 다르게 상태 값이 변경될 때마다 컴포넌트가 자동으로 다시 렌더링 되므로, 화면에 변경 사항이 즉시 반영된다.
React에서 데이터는 크게 두 가지 방법으로 관리된다.
개념 | 설명 | 변경 가능 여부 | 데이터의 소유자 |
Props | 부모 컴포넌트에서 자식 컴포넌트로 전달되는 데이터 | ❌ (읽기 전용) | 부모 컴포넌트 |
State | 컴포넌트 내부에서 관리되는 데이터 | ✅ (변경 가능) | 해당 컴포넌트 |
즉, props는 부모가 제어하는 데이터이고, state는 컴포넌트 자체가 관리하는 데이터이다.
React는 화면을 효율적으로 업데이트하기 위해 필요한 경우에만 리렌더링(Rendering)을 수행한다.
다음과 같은 상황에서 컴포넌트는 다시 렌더링 된다.
1. 컴포넌트 상태(State)가 변경되었을 때
useState로 선언된 상태가 변경되면 컴포넌트가 리렌더링 된다.
즉, setState가 호출되면 React가 변경 사항을 감지하고, 해당 컴포넌트를 다시 그림(DOM 업데이트).
2. Props가 변경되었을 때
부모 컴포넌트에서 전달된 props 값이 변경되면, 해당 값을 사용하는 자식 컴포넌트도 리렌더링 된다.
3. 부모 컴포넌트가 리렌더링 되었을 때
부모가 다시 렌더링 되면, 해당 부모에 포함된 모든 자식 컴포넌트도 함께 리렌더링 된다.
리렌더링이 필요한 경우 쉽게 이해하기
👉 "화면에 변화를 적용해야 하는가?"를 기준으로 생각하면 된다.
✔ 상태(State)가 변경됨 → UI에 반영해야 하므로 리렌더링
✔ Props가 변경됨 → 변경된 데이터를 반영해야 하므로 리렌더링
✔ 부모 컴포넌트가 리렌더링 됨 → 포함된 자식 컴포넌트도 함께 리렌더링
리렌더링의 단점
리렌더링은 UI 업데이트를 위한 필수 과정이지만, 불필요한 리렌더링이 많아지면 성능이 저하될 수 있다.
예를 들어, 상태가 불필요하게 자주 변경되거나, 변경이 필요 없는 컴포넌트까지 리렌더링 된다면 성능이 나빠질 수 있다.
이러한 문제를 해결하기 위해 React에서는 최적화 기법(예: useMemo, React.memo)을 제공한다. 이에 대한 자세한 내용은 추후 설명할 예정이다.
2.4-3. Hook의 규칙
상태를 생성하고 관리하기 위해 useState라는 Hook을 사용한다.
Hook이란?
Hook은 React 함수형 컴포넌트에서 상태(state)와 생명주기(lifecycle) 기능을 사용할 수 있도록 도와주는 기능이다.
이전에는 클래스형 컴포넌트에서만 상태를 관리할 수 있었지만, Hook을 사용하면 함수형 컴포넌트에서도 상태를 관리할 수 있지만 사용할 때 지켜야 하는 규칙이 있다.
1. 반드시 함수형 컴포넌트 내에서 사용해야 한다 : 클래스형 컴포넌트에서는 사용할 수 없다.
2. 조건문이나 반복문 안에서 사용하면 안 된다 : 항상 컴포넌트의 최상위 레벨에서 호출해야 한다.
3. Hook은 React 함수(useState, useEffect, useMemo 등)로 시작해야 한다.
Hook을 사용할 수 있는 곳은 2개인데, 하나는 커스텀 컴포넌트를 정의하는 함수 내부이다.
이는 우리가 Button 컴포넌트에서 Button 함수 내부를 의미한다. 커스텀 컴포넌트를 만들 수 있듯 Hook도 우리가 원하는 형태로 만들 수 있다. 이때 커스텀 Hook을 정의하는 함수가 Hook을 사용할 수 있는 두 번째 위치이다.
react는 Hook을 호출되는 순서대로 저장하고, 컴포넌트가 리렌더링 될 때도 저장된 순서대로 Hook을 호출한다.
따라서 Hook이 항상 같은 순서로 호출되지 않으면 React가 올바르게 상태를 관리할 수 없다. 예를 들어, 함수 내부나 조건문 안에서 Hook을 호출하면 특정 조건에 따라 Hook이 실행되지 않을 수 있다.
이렇게 되면 React가 저장해 둔 Hook의 순서와 실제 호출된 순서가 달라지게 되어 오류가 발생할 가능성이 높아진다.
이러한 문제를 방지하기 위해 Hook은 반드시 함수의 최상위에서 호출해야 한다.
또한, 컴포넌트 이름이 대문자로 시작하는 것처럼, Hook의 이름에도 규칙이 있다.
Hook은 반드시 "use"로 시작해야 하며, 이렇게 하면 React가 이를 Hook으로 인식하고 내부적으로 상태를 관리할 수 있게 된다.
따라서 "use"로 시작하는 함수가 있다면, 이는 React Hook일 가능성이 높다고 생각하면 된다. 🚀
2.4-4. Hook과 ESLint
Hook을 사용할 때 ESLint를 설정해 발생할 실수를 막을 수 있다.
ESLint에 Hook 규칙을 설정하기 위해서는 eslint-plugin-react-hooks라는 플러그인을 설치해야 한다. eslint-plugin-react-hooks 플러그인은 React에서 Hook을 올바르게 사용하는지를 검사하는 역할을 한다.
이 플러그인은 두 가지 주요 규칙을 제공한다. 첫 번째는 rules-of-hooks로, Hook이 항상 최상위에서 호출되도록 강제한다. 두 번째는 exhaustive-deps로, useEffect에서 의존성 배열을 올바르게 작성했는지를 검사하여 불필요한 리렌더링을 방지한다. 이를 통해 Hook을 사용할 때 발생할 수 있는 실수를 줄이고, 코드의 안정성을 높일 수 있다.
아래 명령어를 터미널에 입력한다.
npm install -D eslint-plugin-react-hooks
설치가 완료되었다면 아래와 같이 eslint.config.mjs 파일을 변경하자.
이때 eslint.config.mjs 파일은 최신 ESLint의 Flat Config 방식을 따르고 있기 때문에 기존의. eslintrc.json 형식에서 사용하는 "extends" 키를 직접 사용할 수 없다. 대신, pluginReactHooks를 import 하고 pluginReactHooks.configs.recommended를 추가하는 방식으로 설정해야 한다.
/Users/apple/React/RN/rn-calc/eslint.config.mjs
import globals from 'globals';
import pluginJs from '@eslint/js';
import pluginReact from 'eslint-plugin-react';
import pluginReactHooks from 'eslint-plugin-react-hooks'; // React Hooks 플러그인 추가
/** @type {import('eslint').Linter.Config[]} */
export default [
{ files: ['**/*.{js,mjs,cjs,jsx}'] },
{ languageOptions: { globals: globals.node } },
{
rules: {
'no-console': 'off', // console.log 허용
},
},
pluginJs.configs.recommended,
pluginReact.configs.flat.recommended,
pluginReact.configs['jsx-runtime'], // React 17 버전 호환
pluginReactHooks.configs.recommended, // React Hooks 규칙 추가
];
2.4-5. useState Hook으로 상태 관리하기
useState는 리액트에서 상태를 관리하기 위한 Hook이다. 상태는 동적으로 변하는 데이터를 저장하고, 값이 변경될 때마다 컴포넌트가 리렌더링 되도록 한다. useState를 사용하면 함수형 컴포넌트에서도 상태를 관리할 수 있다.
사용방법은 아래와 같다.
const [count, setCount] = useState(0);
이 코드는 count라는 상태 변수를 생성하고, 초기값을 0으로 설정한다. setCount는 count 값을 변경하는 함수이다.
이렇게 useState는 배열 구조 분해 할당을 사용하여 두 개의 값을 반환하는데, 이를 활용하면 코드가 간결해진다.
배열 구조 분해 할당을 사용하면 배열에서 특정 값을 쉽게 추출할 수 있다. 이때 배열 구조 분해 할당이란 useState를 사용할 때 반환되는 배열을 상태변수와 상태 변경함수로 나누어 받는 문법을 말한다.
객체 구조 분해는 객체에서 원하는 데이터를 받아올 때 프로퍼티와 같은 이름으로 받아와야 하지만 배열 구조 분해에서 배열은 값의 나열이므로 이름을 신경 쓸 필요가 없는 한편 값의 위치를 신경 써야 한다.
구분 | 배열 구조 분해 | 객체 구조 분해 |
기본 개념 | 배열에서 특정 인덱스의 값을 추출 | 객체에서 특정 속성(Property)을 추출 |
사용하는 기호 | [] 대괄호 사용 | {} 중괄호 사용 |
순서 중요 여부 | 요소의 순서가 중요함 | 속성 이름이 중요하고 순서는 상관없음 |
예제 코드 | const [a, b] = [1, 2]; | const { name, age } = { name: 'John', age: 25 }; |
다른 이름 할당 | 불가능 (인덱스 고정) | 가능 (const { name: myName } = obj;) |
기본값 설정 | const [a = 10, b = 20] = [5]; // a=5, b=20 | const { name = "John" } = {}; // name="John" |
이제 App 컴포넌트에 useState를 사용해 결괏값을 관리해 보자.
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View } from 'react-native';
import Button, { ButtonTypes } from '../components/Button';
import { useState } from 'react';
const App = () => {
// let result = 0; 리렌더링 되지 않는 자료형
const [result, setResult] = useState(0);
return (
<View style={styles.container}>
<StatusBar style="auto" />
<Text style={styles.text}>{result}</Text>
<Button
title="+"
onPress={() => {
// result = result + 1;
setResult(result + 1);
console.log('+ : ', result);
}}
buttonStyle={styles.button}
buttonType={ButtonTypes.OPERATOR}
/>
<View style={{ paddingVertical: 10 }}></View>
<Button
title="-"
onPress={() => {
// result = result - 1;
setResult(result - 1);
console.log('- : ', result);
}}
buttonStyle={styles.button}
buttonType={ButtonTypes.OPERATOR}
/>
</View>
);
};
// 스타일 정의
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
text: {
fontSize: 60,
fontWeight: '700',
// color: 'green',
},
button: {
width: 100,
height: 100,
},
});
export default App;
App 컴포넌트에 useState 적용하기
1. 왜 result 변수뿐만 아니라 setResult가 필요할까?
React에서는 useState를 사용하여 상태를 관리한다. useState는 상태 값과 해당 값을 변경할 수 있는 함수(setter)를 함께 반환하는 배열이다. 따라서, result와 이를 변경하는 함수인 setResult가 함께 필요하다.
코드에서 const [result, setResult] = useState(0);을 보면 result는 현재 상태 값을 의미하며, setResult는 result를 변경하는 함수이다. 이를 통해 React는 값이 변경될 때 자동으로 컴포넌트를 다시 렌더링(rendering)하여 화면을 업데이트한다.
이때 setResult는 React Native의 기본 제공 함수가 아니라, useState를 호출할 때 자동으로 생성되는 함수이다. 즉, React의 useState Hook이 반환하는 함수이며, 상태 값을 변경하는 역할을 한다.
꼭 useState를 사용하기 위해 반드시 두 개의 요소를 가져야 하는 것은 아니지만, React의 상태 관리 원칙에 따라 두 개의 요소로 받아야 올바르게 동작한다. 아래 코드의 경우, 이렇게 두 개의 요소로 받지 않으면 useState의 상태 변경 기능을 사용할 수 없다.
2. 두 값을 담은 배열이 왜 useState(0);으로 초기화될까?
useState는 배열 구조 분해(destructuring)를 사용하여 상태 값을 선언한다. useState(0);에서 0은 result의 초기값이다.
즉, 컴포넌트가 처음 렌더링될 때 result는 0으로 설정된다. 이때 useState(0);의 괄호 안에는 항상 하나의 초기값만 들어간다. useState는 상태 값 하나를 관리하는 것이 원칙이기 때문이다.
따라서 만약 상태 값을 하나 더 추가하고 싶다면 useState를 여러 번 호출해야 한다‼️
예시:
const [result, setResult] = useState(0); // 숫자 상태
const [name, setName] = useState("John"); // 문자열 상태
이렇게 각각의 상태 값을 개별적으로 관리해야 한다.
즉, useState의 괄호 안에 두 개 이상의 값을 넣는 것이 아니라, useState를 여러 번 호출하여 각각 관리하는 것이 올바른 방식이다.
const [result, setResult] = useState(0);
위에서 언급한 규칙을 반영하여, 위 코드는 다음과 같은 의미를 가진다:
- result: 현재 상태 값, 초기값은 0
- setResult: 상태 값을 변경할 때 사용하는 함수
이때, setResult는 useState가 자동으로 생성하는 상태 변경 함수(setter function)이며, 상태를 직접 변경하는 것이 아니라 setResult 함수를 호출하여 상태를 변경해야 한다.
React의 useState는 내부적으로 다음과 같은 동작을 한다:
1. useState(초기값) 호출 -> 컴포넌트 내부에서 상태 값을 저장할 공간 생상.
2. 상태 값을 읽을 수 있도록 첫 번째 요소(예: result)를 반환 -> 상태를 변경할 수 있는 두 번째 요소(예: setResult)를 반환.
3. setResult(새로운 값) 호출 -> React는 내부적으로 상태 값을 변경 -> 컴포넌트를 재렌더링 - 변경된 상태 화면 반영.
React 내부적으로는 다음과 같은 방식으로 작동한다:
function useState(initialValue) {
let state = initialValue; // 상태를 저장할 변수
function setState(newValue) {
state = newValue; // 상태 값 변경
reRender(); // 변경된 상태를 반영하기 위해 컴포넌트를 다시 렌더링
}
return [state, setState]; // 상태 값과 상태 변경 함수를 반환
}
따라서 setResult(result + 1);을 호출하면 다음과 같다:
result + 1을 새로운 상태 값으로 설정 ➡️ React가 내부적으로 상태를 변경하고, 다시 렌더링을 수행 ➡️ 렌더링 된 UI에서 변경된 값이 적용
3. 기존의 result = result + 1; 방법이 아니라 setResult(result + 1);을 사용할 때, result 변수는 리렌더링이 되는가?
기존 방식인 result = result + 1;를 사용하면 React는 이 변수가 변경되었는지 알지 못한다. 즉, React의 렌더링 시스템과 무관한 일반 변수로 동작하기 때문에 값이 변경되어도 화면이 자동으로 업데이트되지 않는다.
하지만 setResult(result + 1);을 사용하면 React는 result의 상태가 변경되었음을 인지하고 컴포넌트를 다시 렌더링 하여 화면을 업데이트한다. 즉, useState를 사용하면 result 값이 변경될 때마다 자동으로 화면이 갱신되는 효과가 발생한다.
setResult(result + 1); // 상태를 변경 → React가 리렌더링 하여 화면을 업데이트함
이때, 상태 값이 즉시 변경되는 것이 아니라 React의 상태 변경 프로세스에 따라 렌더링 후 변경된다. 따라서, console.log(result);를 실행하면 변경된 값이 아니라 이전 상태 값이 출력될 수 있다. 상태 값이 변경되었는지 확인하려면 useEffect와 같은 추가적인 React Hook을 활용해야 한다. 📌📌📌
App 컴포넌트에 useState 적용 버그
앞선 과정을 통해 버튼을 누를 때마다 변경된 결과가 잘 나타나는 것을 확인할 수 있다.
하지만 화면의 결과와 터미널에 출력되는 result의 결과가 같지 않는데, 이는 상태 변수를 수정하는 함수가 비동기적으로 작동하기 때문이다‼️
![]() |
![]() |
함수를 호출해서 상태를 변경해도 즉각적으로 반영되지 않기 때문에 console.log를 호출하는 시점에서 result가 변경되기 전 값이 된다.
비동기적으로 작동하는 것을 확인하기 위해 더하기 버튼을 누르면 setResult를 연속으로 2번 호출하도록 수정하면, 우리의 논리로는 첫 번째 setResult에서 1이 증가하고 두번째 setResult에서 1이 증가하여 총 2가 증가해야하지만 실제로 1만 증가하며 터미널 출력 결과도 이전과 변화 없이 "0, 1, 2,,,"로 증가한다.
이는 다음의 과정에서 기인한다.
- 현재 result값이 0이라고 가정한다.
- 첫번째 setResult를 통해 result 값(0)에서 1을 더한 값(1)으로 상태를 변경한다.
- 두 번째 setResult가 호출되기 전에 상태는 변경되지 않는다.
- 두 번째 setResult를 통해 result 값(0)에서 1을 더한 값(1)로 상태를 변경한다.
- console.log가 호출되기 전까지 상태는 변경되지 않는다.
- console.log를 통해 터미널에 현재 result 값(0)이 출력된다.
React의 useState는 비동기적으로 작동하기 때문에 setResult를 호출한다고 해서 즉시 result 값이 변경되지 않는다. 이로 인해 console.log를 거칠 때 setResult를 여러 번 호출해도 기대한 값으로 즉시 업데이트되지 않는다.
왜 setResult를 두 번 호출해도 값이 1만 증가할까?
React는 상태를 즉시 업데이트하지 않고 배치(batch) 처리한다. 즉, 한 번의 렌더링 사이클에서 여러 개의 setState 호출이 있더라도, 마지막에 한 번만 상태를 업데이트한다.
<Button
title="+"
onPress={() => {
setResult(result + 1); // 첫 번째 호출
setResult(result + 1); // 두 번째 호출
console.log("현재 result 값:", result);
}}
/>
1. 현재 result = 0인 상태에서 + 버튼 클릭
첫 번째 setResult(result + 1); 호출 → result가 1로 업데이트될 예정
두 번째 setResult(result + 1); 호출 → 여전히 result는 0이므로 다시 1로 업데이트될 예정
하지만 두 개의 setResult 호출이 같은 result 값(0)을 기반으로 실행되었기 때문에 결국 한 번만 증가
2. console.log("현재 result 값:", result);가 호출될 때
setResult는 즉시 실행되지 않으며, 상태 변경이 반영되기 전에 console.log가 실행된다. 따라서 console.log는 여전히 이전 값인 0을 출력하며, 렌더링 후에야 result 값이 1로 변경됨
React는 비동기적이며, 배치(batch) 업데이트를 수행하기 때문에 렌더링 시점은 console.log 실행 이후에 발생하게 된다.
그럼, result 값은 언제 변경될까?
React는 상태 변경이 요청되면 즉시 변경하지 않고, 렌더링 사이클이 끝난 후 새로운 값으로 렌더링을 트리거한다. 즉, 렌더링이 끝나야 변경된 상태 값이 반영된다.
✔️ 렌더링 시점
- setState(즉, setResult)를 호출하면 React는 비동기적으로 상태 변경을 스케줄링
- 현재 실행 중인 함수(이벤트 핸들러)가 종료되면 React가 새로운 렌더링을 트리거
- 새로운 렌더링이 수행될 때 result 값이 업데이트된 상태로 반영됨
- 즉, console.log 실행 이후, 렌더링이 진행되면서 result 값이 변경된 UI가 화면에 반영
App 컴포넌트에 useState 적용 버그 수정
React에서 상태를 변경할 때 현재 상태 값을 기반으로 새로운 값을 설정해야 하는 경우가 많다. 하지만 단순히 setState에 상태 변수를 직접 사용하면 비동기적인 업데이트 과정에서 예상과 다른 결과가 발생할 수 있다. 이러한 문제를 방지하기 위해 React는 상태 변경 함수에 함수를 전달하는 방식을 제공하는데, 이를 함수형 업데이트(functional update)라고 한다.
함수형 업데이트(functional update)는 React에서 상태를 변경할 때 현재 상태 값을 안전하게 참조하여 업데이트하는 방법이다. React의 setState는 비동기적으로 동작하기 때문에 여러 번 상태 변경을 호출하면 이전 상태를 올바르게 반영하지 못하는 문제가 발생하는데, 이를 방지하기 위해 상태 변경 함수에 함수를 전달하면 해당 함수의 첫 번째 매개변수로 최신 상태 값이 자동으로 전달된다. 이를 이용하면 상태가 여러 번 변경될 때도 정확한 값을 기반으로 업데이트할 수 있다.
1. 함수를 전달할 때 반드시 새로운 상태 값을 반환해야 한다.
2. 만약 함수에서 값을 반환하지 않으면 상태는 undefined가 되므로 주의해야 한다.
3. 상태 변경 함수에 함수를 전달하면 해당 함수의 첫 번째 매개변수로 현재 상태가 자동으로 전달된다.
1️⃣ 기존 방식의 문제점
setResult(result + 1);
setResult(result + 1);
위 코드에서 setResult(result + 1);을 연속으로 두 번 호출하면 예상과 달리 result 값이 2가 아니라 1만 증가한다.
이는 두 개의 setResult가 동일한 기존 상태(result)를 참조하여 실행되기 때문이다.
따라서 result의 변경이 렌더링 전에 반영되지 않아 두 번째 setResult가 여전히 같은 초기 값에서 업데이트를 수행하게 된다
2️⃣ 함수형 업데이트를 사용한 해결 방법
setResult((prevResult) => {
return prevResult + 1;
});
위처럼 setResult에 함수를 전달하면 React가 이전 상태(prevResult)를 자동으로 전달해 주며, 이를 기반으로 새로운 상태를 설정할 수 있다. 이를 사용하면 setResult가 연속으로 호출되더라도 각 호출이 최신 상태를 참조하므로, 상태 변경이 누락되지 않는다.
3️⃣ 최종 코드
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View } from 'react-native';
import Button, { ButtonTypes } from '../components/Button';
import { useState } from 'react';
const App = () => {
const [result, setResult] = useState(0);
console.log('Rendering: ', result); // 리렌더링 시마다 result 값 확인
return (
<View style={styles.container}>
<StatusBar style="auto" />
<Text style={styles.text}>{result}</Text>
<Button
title="+"
onPress={() => {
setResult((prevState) => {
console.log('prevState 1:', prevState);
return prevState + 1;
});
// setResult((prevState) => {
// console.log('prevState 2:', prevState);
// return prevState + 1;
// });
}}
buttonStyle={styles.button}
buttonType={ButtonTypes.OPERATOR}
/>
<View style={{ paddingVertical: 10 }}></View>
<Button
title="-"
onPress={() => {
setResult((prevState) => {
console.log('prevState 1:', prevState);
return prevState - 1;
});
// setResult((prevState) => {
// console.log('prevState 2:', prevState);
// return prevState - 1;
// });
}}
buttonStyle={styles.button}
buttonType={ButtonTypes.OPERATOR}
/>
</View>
);
};
// 스타일 정의
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
text: {
fontSize: 60,
fontWeight: '700',
},
button: {
width: 100,
height: 100,
},
});
export default App;
최종 구현 영상
![]() |
![]() |
'App' 카테고리의 다른 글
[React Native] #2-2. 계산기 인터페이스 기초 (1) | 2025.02.11 |
---|---|
[React Native] #2-1. 계산기 만들기 초기 설정 (1) | 2025.02.04 |
[React Native] #1-2. 리액트 네이티브 프로젝트 생성하기 (2) | 2025.02.02 |
[React Native] #1-1. 리액트 네이티브 시작하기 (1) | 2025.02.01 |