Divide & Conquer
(분할 - 정복)의 용이관심사의 분리
라고 부른다.Loose Coupling
(낮은 결합도, 각각의 코드가 서로 얽혀있지 않고 독립적으로 잘 분리되어 있음)과 High Cohesive
(높은 응집도, 유사한 내용끼리 비슷한 위치에 잘 모여 있음)와 같은 특성을 발견할 수 있다.const loginForm = document.getElementsByClassName('loginForm')[0];
const loginBtn = document.getElementById('loginBtn');
function handleInput() {
const idValue = document.getElementById('id').value;
const pwValue = document.getElementById('pw').value;
const isIdValid = idValue.length > 1;
const isPasswordValid = pwValue.length > 1;
const isLoginInputValid = isIdValid && isPasswordValid;
loginBtn.disabled = !isLoginInputValid
loginBtn.style.opacity = isLoginInputValid ? 1 : 0.3;
loginBtn.style.cursor = isLoginInputValid ? 'pointer' : 'default';
}
const init = () => {
loginForm.addEventListener('input', handleInput);
};
init();
function $(selector) {
return document.querySelector(selector)
}
const loginForm = $('.loginForm');
const loginBtn = $('#loginBtn');
const validator = {
id: (id) => id.length > 1,
password: (password) => password.length > 1,
}
function validateForm(form, validator) {
for (const key in form) {
const value = form[key]
const isValid = validator[key](value)
if (!isValid) return false
}
return true
}
function handleButtonActive(button, isFormValid) {
button.disabled = isFormValid ? false : true;
button.style.opacity = isFormValid ? 1 : 0.3;
button.style.cursor = isFormValid ? 'pointer' : 'default';
}
function handleInput() {
const isFormValid = validateForm({
id: $('.id').value,
password: $('.pw').value
}, validator)
handleButtonActive(loginBtn, isFormValid)
}
function init() {
loginForm.addEventListener('input', handleInput);
};
init();
'관심사의 분리'를 프론트엔드 개발에 적용할 수 있다.
Redux와 Create React App의 창시자로 React 생태계에서 아주 유명한 Dan Abramov는 2015년 본인의 블로그에 Presentational - Container
패턴으로 React 컴포넌트를 관리하는 방법을 제안했다. (본문 링크)
Presentational Component : UI Only 컴포넌트 (View
)
const UserList = ({ users }) => {
return (
<ul>
{users.map(user => {
return <li key={user.id}>{user.name}</li>
})}
</ul>
)
}
Container Component : Logic Only 컴포넌트 (Logic
)
class UserListContainer extends React.Component {
state = {
users: []
}
componentDidMount() {
fetchUsers("/users")
.then(res => res.json())
.then(res => this.setState({ users: res.users }))
}
render() {
return <UserList users={this.state.users}>
}
}
이 패턴이 제안될 당시에는 Hooks가 존재하지 않았다. 그래서 함수 컴포넌트는 State와 Effect를 처리할 수 없었고, UI를 그리는 데만 제한적으로 사용되었다. 반면 클래스형 컴포넌트는 State와 Life Cycle API(Effect에 대응됨)를 사용해 React의 모든 기능을 구현할 수 있었다.
=> 이렇게 View와 Logic을 분리함으로써 컴포넌트는 더 작은 영역에 대해 더 확실한 책임을 지는 여러 개의 컴포넌트로 분할된다.
View에 집중하는 Presentational Component는 단순히 주입 받은 정보를 렌더링할 뿐(ex. (state, props) ⇒ UI
)이다. 주입 받은 정보만 올바르다면 컴포넌트는 항상 올바른 UI를 리턴하게 된다. 로직과 분리되었기 때문에 여러 곳에서 재활용하기도 쉽고, 여러 가지 Container Component와 조합하기도 쉽다.
하지만 Hooks의 등장으로 더이상 이 패턴을 권장하지 않는다.
use
로 시작하는 자바스크립트 함수다.const UserList = ({ users }) => {
const [users, setUsers] = useState([])
useEffect(() => {
fetchUsers("/users")
.then(res => res.json())
.then(res => setUsers(res.users))
}, [])
return (
<ul>
{users.map(user => {
return <li key={user.id}>{user.name}</li>
})}
</ul>
)
}
const UserList = ({ users }) => {
// Logic
const users = useGetUserList()
// View
return (
<ul>
{users.map(user => {
return <li key={user.id}>{user.name}</li>
})}
</ul>
)
}
const useGetUserList = () => {
const [users, setUsers] = useState([])
useEffect(() => {
fetchUsers("/users")
.then(res => res.json())
.then(res => setUsers(res.users))
}, [])
return users
}
UserList
의 내용을 파악해야 하는 입장에서 적절하게 지어진 Custom Hook의 이름과, 그 Hook이 리턴하는 user
라는 이름의 변수를 통해 세부 구현사항은 모르겠지만, 어쨌든 저것이 유저들에 대한 정보라는 사실은 어렵지 않게 알 수 있게 된다.UserList
라는 컴포넌트를 살펴보지 않고 useGetUserList
hook만 집중하여 살펴보면 된다.const UserInput = () => {
const [size, setSize] = useState({ width: 0, height: 0 })
const [position, setPosition] = useState({ x: 0, y: 0 })
const [userInfo, setUserInfo] = useState({
username: "",
id: "",
password: "",
email: ""
})
const handleUserInput = (e) => {
const { name, value } = e.target
setUserInfo(prev => ({ ...prev, [name]: value }))
}
useEffect(() => {
const handleDocumentSize = () => { ... }
document.addEventListener("resize", handleDocumentSize)
return () => {
document.removeEventListener("resize", handleDocumentSize)
}
}, [])
useEffect(() => {
const handleDocumentPosition = () => { ... }
document.addEventListener("resize", handleDocumentPosition)
return () => {
document.removeEventListener("resize", handleDocumentPosition)
}
}, [])
return (
<div>
<input name="username" onChange={handleUserInput} />
<input name="id" onChange={handleUserInput} />
<input name="password" onChange={handleUserInput} />
<input name="email" onChange={handleUserInput} />
<div />
)
}
UserInput
에 Logic을 추출하여 View 만 남았고, 유저 정보와 관련된 useUserInput
과 화면 크기 조절시마다 문서의 크기와 위치를 리턴하는 useDocumentResize
두 가지 로직으로 분리되어 있음을 확인할 수 있다. const UserInput = () => {
const { userInfo, handleUserInfo } = useUserInput()
const { size, position } = useDocumentResize()
return (
<div>
<input name="username" onChange={handleUserInput} />
<input name="username" onChange={handleUserInput} />
<input name="username" onChange={handleUserInput} />
<input name="username" onChange={handleUserInput} />
<div/>
)
}
UserInput
에 한덩어리로 있던 유저 정보 입력 관련한 로직이 useUserInfo
로 분리되었다.const useUserInfo = () => {
const [userInfo, setUserInfo] = useState({
username: "",
id: "",
password: "",
email: ""
})
const handleUserInput = (e) => {
const { name, value } = e.target
setUserInfo(prev => ({ ...prev, [name]: value }))
}
return { userInfo, handleUserInput }
}
UserInput
에 한덩어리로 있던 화면 리사이즈 관련한 로직이 useDocumentResize
로 관심사에 따라 분리되었다.const useDocumentResize = () => {
const [size, setSize] = useState({ width: 0, height: 0 })
const [position, setPosition] = useState({ width: 0, height: 0 })
useEffect(() => {
const handleDocumentSize = () => { ... }
document.addEventListener("resize", handleDocumentSize)
return () => {
document.removeEventListener("resize", handleDocumentSize)
}
}, [])
useEffect(() => {
const handleDocumentPosition = () => { ... }
document.addEventListener("resize", handleDocumentPosition)
return () => {
document.removeEventListener("resize", handleDocumentPosition)
}
}, [])
return { size, position }
}
use
로 시작되어야 한다.use-
로 시작하는 이름을 지어야 한다.import { useParams } from "react-router-dom"
const useGetUserList = () => {
const [users, setUsers] = useState([])
**const { id } = useParams()**
useEffect(() => {
fetchUsers(`/users/${id}`)
.then(res => res.json())
.then(res => setUsers(res.users))
}, [])
return users
}
export const useToggle = (initialValue = false) => {
const [state, setState] = useState(initialValue);
const handleToggle = () => {
setState(prev => !prev)
}
return [state, handleToggle]
};
const useLockBodyScroll = () => {
useLayoutEffect(() => {
document.body.style.overflow = 'hidden';
return () => {
document.body.style.overflow = '';
};
}, []);
};
import { useState, useEffect, useRef } from "react";
function App() {
const ref = useRef();
const [isModalOpen, setModalOpen] = useState(false);
// 활용 예시
**useOnClickOutside(ref, () => setModalOpen(false));**
return (
<div>
{isModalOpen ? (
**<div ref={ref}>**
👋 Hey, I'm a modal. Click anywhere outside of me to close.
</div>
) : (
<button onClick={() => setModalOpen(true)}>Open Modal</button>
)}
</div>
);
}
// Custom Hook이 Side Effect만 일으키고 return 값이 없으면
// 굳이 리턴 값을 정의해주지 않아도 됩니다!
function useOnClickOutside(ref, handler) {
useEffect(
() => {
const listener = (event) => {
if (!ref.current || ref.current.contains(event.target)) {
return;
}
handler(event);
};
document.addEventListener("mousedown", listener);
document.addEventListener("touchstart", listener);
return () => {
document.removeEventListener("mousedown", listener);
document.removeEventListener("touchstart", listener);
};
},
[ref, handler]
);
}
loading
, data
, error
세 가지 값을 리턴하도록 하는 Custom Hookloading
: 데이터가 요청 중일 때는 true, 그 이외의 경우에는 false를 리턴data
: 서버에서 가져온 값. 초기값은 undefined, 완료된 후에는 해당 값을 리턴error
: 요청 도중 error가 일어났을 때 에러 객체를 리턴하고, 그 이외의 경우에는 false를 리턴//usefetch.js
import { useState, useEffect } from "react";
//url을 인자로 받아온다.
const useFetch = url => {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setTimeout(() => {
fetch(url)
.then(res => {
if (!res.ok) {
throw Error("could not fetch the data for that resource");
}
return res.json();
})
.then(_data => {
setData(_data);
setIsPending(false);
setError(null);
})
.catch(err => {
setIsPending(false);
setError(err.message);
});
}, 1000);
}, [url]);
return { data, isLoading, error };
};
export default useFetch;
export default function Component() {
const url = "http://jsonplaceholder.typicode.com/posts"
const { loading, data, error } = useFetch(url)
return <div>{status}</div>
}