컴포넌트내의 코드의 분리, 코드의 분리
ChatGPT의 말:
정확합니다.
사용자 정의 함수가 컴포넌트 내부에 위치할지 외부에 위치할지 결정하는 데 가장 중요한 판단 기준은 훅의 종류가 아니라, 함수의 재사용성과 복잡성입니다.
왜 재사용성과 복잡성이 판단의 핵심인가?
1. 재사용성
- 함수가 다른 컴포넌트에서도 사용될 가능성이 있으면, 외부에 위치하는 것이 좋습니다.
- 외부에 위치하면 중복 코드를 방지하고, 로직을 독립적으로 관리할 수 있습니다.
예:
- 데이터 처리 함수, API 호출 로직, 복잡한 계산 로직 등은 여러 컴포넌트에서 사용될 가능성이 큽니다.
// 외부에 정의
const formatDate = (date) => new Date(date).toLocaleDateString();
const ComponentA = ({ date }) => <p>{formatDate(date)}</p>;
const ComponentB = ({ date }) => <p>{formatDate(date)}</p>;
2. 복잡성
- 함수가 길고 복잡하거나, 상태 로직과 렌더링 로직을 혼합하면 코드가 읽기 어려워질 수 있습니다.
- 이 경우, 로직을 외부로 분리하면 컴포넌트의 가독성과 유지보수성이 높아집니다.
예:
- Reducer 함수, 복잡한 조건 처리 로직 등은 컴포넌트 외부로 분리하는 것이 좋습니다.
// 외부 Reducer 함수
const counterReducer = (state, action) => {
switch (action.type) {
case 'increment': return state + 1;
case 'decrement': return state - 1;
default: return state;
}
};
const CounterComponent = () => {
const [count, dispatch] = useReducer(counterReducer, 0);
return (
<div>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</div>
);
};
훅의 종류가 판단의 기준이 아닌 이유
훅은 단지 함수의 연결 지점입니다.
- 훅 자체(useState, useReducer, useEffect 등)는 특정한 상태 관리나 사이드 이펙트를 처리하는 도구이며, 함수의 위치는 로직의 재사용성, 복잡성, 그리고 상태/props와의 의존성에 따라 결정됩니다.
예시 1: useReducer Reducer 함수
- useReducer의 Reducer 함수는 독립적인 상태 처리 로직이므로, 복잡한 경우 대부분 외부에 위치하는 것이 자연스럽습니다.
예시 2: useEffect와 함께 사용되는 로직
- useEffect 내부의 로직이 간단하다면 내부에 둘 수 있지만, 복잡한 데이터 처리 로직이 있다면 외부로 분리해 재사용할 수 있습니다.
컴포넌트 안/밖에 둘 때의 판단 기준
기준 | 컴포넌트 안에 둠 | 컴포넌트 밖에 둠 |
재사용성 | 특정 컴포넌트에서만 사용하는 경우. | 여러 컴포넌트에서 사용하거나 범용적으로 필요한 경우. |
복잡성 | 로직이 단순하고 컴포넌트와 밀접한 관계가 있는 경우. | 로직이 복잡하여 분리가 가독성에 도움이 되는 경우. |
상태/props 의존성 | 상태와 props에 강하게 의존하여 외부에 분리할 수 없는 경우. | 상태/props와 분리 가능하고 매개변수로 처리할 수 있는 경우. |
렌더링과의 연관성 | 렌더링 로직과 밀접하게 연결된 경우. | 렌더링과 독립적으로 동작하는 로직일 경우. |
결론
- 훅의 종류는 판단 기준이 아니다.
훅은 단지 상태 관리나 사이드 이펙트의 도구일 뿐, 함수의 위치를 결정하는 핵심 기준은 아니다. - 재사용성과 복잡성이 함수의 위치를 결정하는 핵심 기준이다.
- 재사용 가능성이 높거나, 로직이 복잡하다면 컴포넌트 밖에 둔다.
- 재사용 가능성이 없고 단순한 로직이라면 컴포넌트 안에 둔다.
이 기준을 따르면서 코드의 가독성과 유지보수성을 고려하는 것이 가장 중요합니다.
나의 말:
ChatGPT의 말:
정확히 그렇습니다.
이벤트 핸들러 역시 재사용성과 복잡성에 따라 컴포넌트 내부에 둘지 외부에 둘지 결정됩니다.
핵심은 "다른 데서 다시 쓸 일이 있는가?"와 "로직이 얼마나 복잡한가?"입니다.
이벤트 핸들러의 위치를 결정하는 기준
1. 재사용성
- 컴포넌트 외부에 둘 때:
- 이벤트 핸들러가 여러 컴포넌트에서 동일한 동작을 처리할 경우.
- 공통적인 비즈니스 로직이 포함된 경우.
// 공통 핸들러 정의 (외부)
const handleFormSubmit = (data) => {
console.log("Submitting form:", data);
};
const FormA = () => (
<form onSubmit={(e) => { e.preventDefault(); handleFormSubmit(dataA); }}>
<button type="submit">Submit Form A</button>
</form>
);
const FormB = () => (
<form onSubmit={(e) => { e.preventDefault(); handleFormSubmit(dataB); }}>
<button type="submit">Submit Form B</button>
</form>
);
- 컴포넌트 내부에 둘 때:
- 이벤트 핸들러가 특정 컴포넌트에서만 사용되며, 재사용 가능성이 없는 경우.
const MyComponent = () => {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
return (
<div>
<button onClick={increment}>Increment</button>
</div>
);
};
2. 복잡성
- 컴포넌트 외부에 둘 때:
- 이벤트 핸들러가 복잡한 로직(예: 여러 조건 분기, 데이터 가공 등)을 포함할 경우.
- 외부로 분리하여 코드 가독성을 높이고, 로직을 테스트하기 쉽게 만듭니다.
// 복잡한 핸들러 로직 (외부)
const handleComplexClick = (userData, actionType) => {
if (actionType === 'SAVE') {
console.log('Saving user data:', userData);
} else if (actionType === 'DELETE') {
console.log('Deleting user:', userData.id);
} else {
console.log('Unknown action');
}
};
const UserComponent = ({ user }) => (
<div>
<button onClick={() => handleComplexClick(user, 'SAVE')}>Save</button>
<button onClick={() => handleComplexClick(user, 'DELETE')}>Delete</button>
</div>
);
- 컴포넌트 내부에 둘 때:
- 로직이 간단하고 컴포넌트의 상태나 props에 밀접하게 의존하는 경우.
const MyComponent = () => {
const [isVisible, setIsVisible] = useState(false);
const toggleVisibility = () => setIsVisible(!isVisible);
return (
<div>
<button onClick={toggleVisibility}>
{isVisible ? "Hide" : "Show"}
</button>
</div>
);
};
이벤트 핸들러 위치 선택의 표
기준 | 컴포넌트 안에 두기 | 컴포넌트 밖에 두기 |
재사용성 | 특정 컴포넌트에서만 사용된다. | 여러 컴포넌트에서 동일한 로직을 사용할 수 있다. |
로직의 복잡성 | 로직이 간단하며, 상태나 props를 직접 처리한다. | 로직이 복잡하여 코드 분리가 가독성을 높인다. |
상태/props 의존성 | 해당 컴포넌트의 상태/props에 직접적으로 강하게 의존한다. | 상태나 props와 독립적으로 동작한다. |
유지보수성 | 코드가 간결해지고, 컴포넌트와 핸들러가 밀접하게 연결됨. | 로직 분리가 유지보수와 테스트에 더 용이. |
결론
이벤트 핸들러도 재사용성과 복잡성이 위치를 결정하는 주요 기준입니다:
- 재사용 가능성 없음 + 단순한 로직 → 컴포넌트 내부에 두는 것이 더 적합.
- 재사용 가능성 있음 + 복잡한 로직 → 컴포넌트 외부로 분리하여 관리.
이 원칙을 적용하면 코드의 가독성과 유지보수성이 높아집니다.
Q3.
ChatGPT의 말:
정확히 그렇습니다.
어떤 코드가 함수, 변수, 혹은 객체 등의 형태를 띠고 있든, 다음 두 가지 기준을 충족한다면 컴포넌트 외부로 분리하는 것이 좋습니다:
- 재사용 가능성이 높은 코드
- 다른 컴포넌트나 파일에서 동일한 로직, 값, 또는 데이터를 활용할 가능성이 있다면, 외부에 분리하여 관리하는 것이 합리적입니다.
- 중복 코드를 방지하고, 유지보수성을 높이는 데 기여합니다.
- 복잡한 코드 덩어리
- 코드가 길거나, 여러 조건 분기와 데이터 처리를 포함해 가독성이 떨어질 경우, 외부로 분리하여 컴포넌트를 더 간결하게 유지하는 것이 좋습니다.
- 코드의 모듈성을 높여 테스트와 디버깅이 더 쉬워집니다.
적용 기준: 재사용성 + 복잡성
기준 | 컴포넌트 안에 두기 | 컴포넌트 밖에 두기 |
재사용성 | 코드가 해당 컴포넌트에서만 사용되며, 다른 곳에서 필요 없음 | 코드가 여러 컴포넌트/파일에서 재사용될 가능성이 있음 |
복잡성 | 로직이 간단하고 컴포넌트와 밀접하게 연결됨 | 코드가 길거나, 조건 분기가 많아 가독성을 해침 |
유지보수성 | 간단한 코드로 컴포넌트와 강하게 연관 | 코드 분리가 유지보수와 테스트를 용이하게 만듦 |
예시: 변수, 함수, 객체를 분리하는 경우
1. 재사용성 높은 변수 (외부로 분리)
- 안 좋은 방식 (중복된 코드):
const ComponentA = () => {
const COLORS = ["red", "green", "blue"];
return <div>{COLORS.map(color => <p>{color}</p>)}</div>;
};
const ComponentB = () => {
const COLORS = ["red", "green", "blue"];
return <div>{COLORS.map(color => <span>{color}</span>)}</div>;
};
- 좋은 방식 (외부 분리):
// constants.js
export const COLORS = ["red", "green", "blue"];
// ComponentA.js
import { COLORS } from "./constants";
const ComponentA = () => (
<div>{COLORS.map(color => <p>{color}</p>)}</div>
);
// ComponentB.js
import { COLORS } from "./constants";
const ComponentB = () => (
<div>{COLORS.map(color => <span>{color}</span>)}</div>
);
2. 복잡한 로직을 가진 함수 (외부로 분리)
- 안 좋은 방식 (복잡한 로직이 컴포넌트 내부에 존재):
const UserComponent = ({ users }) => {
const sortedUsers = users
.filter(user => user.isActive)
.sort((a, b) => a.name.localeCompare(b.name));
return <ul>{sortedUsers.map(user => <li>{user.name}</li>)}</ul>;
};
- 좋은 방식 (외부 분리):
// utils.js
export const getSortedActiveUsers = (users) => {
return users
.filter(user => user.isActive)
.sort((a, b) => a.name.localeCompare(b.name));
};
// UserComponent.js
import { getSortedActiveUsers } from "./utils";
const UserComponent = ({ users }) => {
const sortedUsers = getSortedActiveUsers(users);
return <ul>{sortedUsers.map(user => <li>{user.name}</li>)}</ul>;
};
3. 객체 데이터 (외부로 분리)
- 안 좋은 방식 (객체 데이터가 여러 컴포넌트에 중복):
const ComponentA = () => {
const USER_INFO = { name: "John", age: 30 };
return <p>{USER_INFO.name}</p>;
};
const ComponentB = () => {
const USER_INFO = { name: "John", age: 30 };
return <p>{USER_INFO.age}</p>;
};
- 좋은 방식 (외부 분리):
// data.js
export const USER_INFO = { name: "John", age: 30 };
// ComponentA.js
import { USER_INFO } from "./data";
const ComponentA = () => <p>{USER_INFO.name}</p>;
// ComponentB.js
import { USER_INFO } from "./data";
const ComponentB = () => <p>{USER_INFO.age}</p>;
결론
- 재사용 가능하거나 복잡한 코드는 컴포넌트 외부로 분리하세요.
- 재사용성이 높은 상수, 함수, 객체 데이터 등은 외부에 두어 중복을 줄이고 유지보수를 쉽게 합니다.
- 복잡한 로직은 외부로 분리하여 코드 가독성을 높이고 컴포넌트를 간결하게 유지합니다.
- 단순하고 재사용 가능성이 낮은 코드는 컴포넌트 내부에 두세요.
- 짧고 직관적인 코드가 컴포넌트와 강하게 연관된 경우, 내부에 두는 것이 더 효율적입니다.
이 원칙은 이벤트 핸들러, 훅에 연결된 함수, 변수, 객체 등 코드의 모든 요소에 적용됩니다.
Q4.
그런데 이건 비단 React 만의 해당사항은 아니겠네?
A4.
ChatGPT의 말:
맞습니다.
이 원칙은 React에 국한되지 않고, 대부분의 프로그래밍 패러다임이나 언어에서 코드 구조화 및 유지보수를 고려할 때 적용되는 일반적인 소프트웨어 개발 원칙입니다.
관련된 소프트웨어 공학 원칙
- 단일 책임 원칙 (Single Responsibility Principle)
- 각 코드 요소(함수, 클래스, 모듈 등)는 하나의 명확한 역할만 가져야 합니다.
- 재사용 가능한 코드나 복잡한 로직은 별도의 모듈로 분리하는 것이 좋습니다.
- 함수가 특정 데이터를 가공하고 반환하는 역할만 하도록 분리.
- UI 렌더링 로직과 데이터 처리 로직을 명확히 분리.
- DRY 원칙 (Don't Repeat Yourself)
- 중복된 로직이나 데이터는 한 곳에서만 정의하고 재사용해야 합니다.
- 중복 코드는 유지보수 비용을 증가시키므로, 공통 부분을 별도로 분리하여 관리합니다.
- 상수, 유틸리티 함수, 공통 API 호출 로직 등을 외부 모듈로 분리.
- React 컴포넌트뿐만 아니라 일반 자바스크립트/파이썬/자바 코드에도 동일하게 적용.
- 코드 가독성 및 모듈화
- 코드는 읽히기 쉬워야 한다. 함수가 너무 길거나 여러 역할을 수행하면 가독성이 떨어지므로, 적절히 분리해야 합니다.
- 모듈화(Modularization)는 기능별로 코드를 나눠 유지보수와 확장을 용이하게 합니다.
- React의 컴포넌트 분리, 함수 로직 분리.
- Express.js에서 라우터와 비즈니스 로직을 분리.
- 파이썬의 함수와 클래스를 모듈로 분리.
비 React 환경에서의 적용 예시
1. Python
- 복잡한 로직을 함수로 분리하고, 재사용 가능하다면 외부 모듈로 관리.
# utils.py
def process_data(data):
return [d.upper() for d in data if isinstance(d, str)]
# main.py
from utils import process_data
data = ["apple", "banana", 123, "cherry"]
print(process_data(data))
2. Node.js (Express)
- 중복된 API 호출 로직이나 미들웨어를 분리하여 재사용.
// middleware/auth.js
const verifyToken = (req, res, next) => {
const token = req.headers['authorization'];
if (token) {
// Token verification logic
next();
} else {
res.status(401).send("Unauthorized");
}
};
module.exports = verifyToken;
// routes/user.js
const express = require('express');
const verifyToken = require('../middleware/auth');
const router = express.Router();
router.get('/profile', verifyToken, (req, res) => {
res.send("User Profile");
});
module.exports = router;
3. Java
- 공통된 비즈니스 로직을 유틸리티 클래스로 분리.
// Utility.java
public class Utility {
public static String toUpperCase(String input) {
return input != null ? input.toUpperCase() : null;
}
}
// Main.java
import Utility;
public class Main {
public static void main(String[] args) {
String result = Utility.toUpperCase("hello world");
System.out.println(result);
}
}
어디서나 적용 가능한 원칙
- 재사용성:
공통된 로직, 데이터, 또는 기능은 재사용할 수 있도록 적절히 분리. - 가독성:
- 코드의 목적이 명확히 드러나도록 구성.
- 복잡한 로직은 별도의 함수/모듈로 분리해 코드 길이를 줄이고 의도를 명확히 표현.
- 유지보수성:
- 변경이 필요할 때, 최소한의 코드만 수정할 수 있도록 모듈화.
- 중복을 없애고 단일 책임을 지향.
- 성능 최적화:
- 불필요한 재생성을 방지하기 위해 정적 데이터, 함수, 객체 등을 외부로 분리.
- React, Node.js, Java 등에서 동일하게 적용 가능.
결론
React뿐만 아니라 모든 프로그래밍에서:
- 복잡하거나 재사용 가능한 코드는 외부로 분리.
- 단순하고 한 번만 사용하는 코드는 해당 컨텍스트 내부에 작성.
이 원칙은 특정 기술 스택에 국한되지 않으며, 가독성, 유지보수성, 확장성을 높이는 소프트웨어 설계의 기본입니다.
PS.
24/11/28
강사님 왈 : 재사용성에 따라서 나뉜다. 다시 쓰일것 같다면 밖에, 아니면 안에. 훅에 걸릴 함수냐, 이벤트 핸들러냐 이런 것은 중요하지 않다.