본문으로 건너뛰기

React / React Native

Overview

이 페이지의 목적은 React 및 React Native에서 사용할 수 있는 베스트 프랙티스와 팁을 예제와 함께 설명하고, 이를 실제 앱에서 어떻게 적용할 수 있을지 보여주는 것입니다.


Rules of Hooks

Hooks는 JavaScript 함수처럼 정의되지만, 특별한 규칙이 있는 재사용 가능한 UI 로직입니다.


TypeScript

대규모 애플리케이션에서는 JavaScript보다는 TypeScript를 사용하는 것이 일반적인 선택입니다. TypeScript는 정적 타입 검사를 제공하여 런타임이 아닌 컴파일 타임에 오류를 발견할 수 있게 해주며, 결과적으로 더 안정적이고 유지 보수가 쉬운 코드를 작성할 수 있게 합니다.


Remove all logs from production app

  • production 앱에서는 console.logreactotron.log를 반드시 제거해야 합니다. 로그 사용은 FPS를 저하시킬 수 있습니다.

Optimization

Memoization in React

무거운 계산 작업이나 배열 및 객체 생성을 매 렌더링마다 반복하지 않도록 memoization이 중요합니다.

Pure Components & React.memo

  • Pure Component 또는 React.memo 컴포넌트는 props가 shallow equal일 경우 리렌더링되지 않습니다.
  • 렌더 함수 안에서 생성한 변수는 매 렌더마다 다시 할당됩니다. 이는 value type에는 문제가 되지 않지만, reference type인 경우 매번 새로운 참조로 간주되어 문제가 됩니다.
  • Pure Component나 React.memo는 이전 props와 현재 props를 shallow 비교하여 리렌더링 여부를 결정합니다.
  • 렌더 함수 내에서 객체를 생성하면 매 렌더마다 새로운 객체가 생성되므로 참조가 달라집니다. 이 때문에 memoization이 필요합니다.
  • 배열이나 객체는 useMemo를 사용하여 참조를 유지하세요.
  • 함수는 useCallback을 사용하여 memoize하세요.

Disclaimer

너무 이른 최적화는 피하세요. 여기에 제시된 예시(useMemo 등)는 단지 개념을 설명하기 위한 것이며, 실제로는 useMemo도 함수와 deps 배열을 할당하고 비교하는 비용이 있으므로 항상 효율적인 것은 아닙니다. 간단한 컴포넌트에서는 그냥 객체나 배열을 넘기는 것이 더 나을 수 있습니다. 하지만 컴포넌트가 복잡해지거나 dependency graph가 커질수록, memoization은 성능 향상에 매우 효과적일 수 있습니다. 항상 적용 전후로 벤치마크하세요.


Do Measure Performance and Profiling

  • Android 앱의 성능 점수를 생성할 때는 FLASHLIGHT를 사용할 수 있습니다.
  • iOS 및 Android 앱의 성능 최적화를 위해 Profiling을 진행할 수 있습니다.

Use Flashlist for listing

  • FlashList는 화면에 보이는 일부 뷰만 생성 및 렌더링하는 recycling view 개념을 기반으로 하여, 성능을 크게 향상시킬 수 있습니다.

Imports order

우리 프로젝트에서는 다음과 같은 import 순서를 따릅니다.

  • Built-in 모듈 먼저 (react, react-native는 built-in 다음)
  • 그다음으로 외부 라이브러리
  • 내부 경로 alias(src/**) import
  • 그다음 상대 경로
  • 각 그룹은 줄 바꿈으로 구분하고 알파벳 순으로 정렬

Examples

Bad

import axios from 'axios'
import { View } from 'react-native'
import React from 'react'
import { COLORS } from 'src/theme/colors'
import useAuth from 'src/hooks/useAuth'
import path from 'path'
import fs from 'fs'
import styles from './styles'
import ParentComponent from '../components/ParentComponent'
import lodash from 'lodash'
import { formatDate } from 'src/utils/date'

Good

// Built-in modules
import fs from 'fs'
import path from 'path'

// react, react-native as part of builtin group (position: after)
import React from 'react'
import { View } from 'react-native'

// External modules
import axios from 'axios'
import lodash from 'lodash'

// Internal modules (e.g., aliased as `src/**`)
import useAuth from 'src/hooks/useAuth'
import { COLORS } from 'src/theme/colors'
import { formatDate } from 'src/utils/date'

// Parent directory imports
import ParentComponent from '../components/ParentComponent'

// Index or same folder imports
import styles from './styles'

Styling

React Native 프로젝트에서는 다양한 방식으로 스타일링할 수 있지만, 우리는 React Native의 StyleSheet API를 사용합니다.

  • 인라인 스타일은 사용하지 마세요.
  • 동적인 스타일이 필요한 경우에만 인라인 스타일을 사용하세요.
  • 성능 최적화를 위해 useMemo를 사용하여 스타일을 메모이제이션하는 것을 고려하세요.

Examples

Bad

<View style={{ backgroundColor: 'blue', padding: 10 }}>
<Text style={{ color: 'white' }}>Hello</Text>
</View>

Good

import { useState, useMemo} from 'react'
import { StyleSheet, View, Text } from 'react-native'

const GoodExample = () => {
const [isVisible, setIsVisible] = useState(false)

return (
<View style={[styles.container, { backgroundColor: isVisible ? 'red' : 'blue' }]}>
<Text style={styles.text}>Hello</Text>
</View>
)
}

// OR

const OptimizedGoodExample = () => {
const [isVisible, setIsVisible] = useState(false)

const containerStyle = useMemo(() => {
return [styles.container, { backgroundColor: isVisible ? 'red' : 'blue' }]
}, [isVisible])

return (
<View style={containerStyle}>
<Text style={styles.text}>Hello</Text>
</View>
)
}

const styles = StyleSheet.create({
container: {
backgroundColor: 'blue',
padding: 10,
},
text: {
color: 'white',
},
})


Initial States

useState 훅은 초기화 함수를 인자로 받을 수 있습니다. 아래 첫 번째 예시는 .find매 렌더마다 실행하지만, 두 번째 예시는 최초 1회만 실행됩니다.

Examples

Bad

const [me, setMe] = useState(users.find((u) => u.id === myUserId));

Good

const [me, setMe] = useState(() => users.find((u) => u.id === myUserId));


Functions

함수를 작성할 때는 다음 원칙을 따르세요.

  • 인라인 함수는 사용하지 마세요.
  • useEffect의 dependency로 사용하거나 memoized 컴포넌트에 prop으로 전달할 경우, 반드시 useCallback으로 감싸야 합니다.

Examples

Bad

// functions in props
return <View onLayout={(layout) => console.log(layout)} />;

// useEffect example
const badExample = () => {
console.log("test");
}

useEffect(()=>{
badExample()
}, [badExample])

Good

// functions in props
const onLayout = (layout) => {
console.log(layout);
}

return <View onLayout={onLayout} />;

// OR optimized
const onLayoutOptimized = useCallback((layout) => {
console.log(layout);
}, []);

return <View onLayout={onLayoutOptimized} />;

// useEffect example
const goodExample = useCallback(() => {
console.log("test");
}, [])

useEffect(()=>{
goodExample()
}, [goodExample])


Arrays

렌더 함수 내에서 filter, map 등의 배열 연산을 수행하면 렌더링마다 다시 계산됩니다. useMemo를 사용해 연산을 최적화하세요.

Examples

Bad

return (
<Text>{users.filter((u) => u.status === 'online').length} users online</Text>
);

Good

const onlineCount = useMemo(
() => users.filter((u) => u.status === 'online').length,
[users]
);
return <Text>{onlineCount} users online</Text>;


Forward-propagating Functions

props로 받은 함수를 사용할 때는 즉시 실행 형태로 wrapping 하지 말고 그대로 전달하세요.

Examples

Bad

function MyComponent(props) {
return <PressableOpacity onPress={() => props.logoutUser()} />;
}

Good

function MyComponent(props) {
return <PressableOpacity onPress={props.logoutUser} />;
}


Lift out of render

객체나 함수가 컴포넌트의 state나 props에 의존하지 않는다면, useMemouseCallback으로 감싸는 것보다 컴포넌트 외부로 분리하는 것이 더 효율적입니다.

Examples

Bad

function MyComponent() {
return <RecyclerListView scrollViewProps={{ horizontal: true }} />;
}

Good

const SCROLL_VIEW_PROPS = { horizontal: true };

function MyComponent() {
return <RecyclerListView scrollViewProps={SCROLL_VIEW_PROPS} />;
}


Avoid Using Index as Key

  • 항상 id처럼 안정적인 고유 식별자를 key로 사용하세요.
  • Math.random()은 절대 사용하지 마세요.
  • indexkey로 사용할 경우:
    • 리스트 변경(add/remove/reorder) 시 버그 발생 가능
    • React가 잘못된 컴포넌트를 재사용할 수 있음
    • TextInput 등에서 포커스 또는 상태 손실 발생 가능
  • indexid를 조합해서 사용하는 것도 추천되지 않지만, 불가피한 경우에는 임시로 허용될 수 있습니다.

Examples

Bad

items.map((item, index) => (
<Text key={index}>{item.name}</Text>
))

<FlatList
data={items}
renderItem={({ item, index }) => (
<Text>{item.name}</Text>
)}
keyExtractor={(_, index) => index.toString()}
/>

Good

items.map(item => (
<Text key={item.id}>{item.name}</Text>
))

<FlatList
data={items}
renderItem={({ item }) => (
<Text>{item.name}</Text>
)}
keyExtractor={item => item.id}
/>

References