1. 관심사의 분리(SoC, Separation of Concerns)
관심사 분리의 원칙
관심사 분리 원칙은 시스템 요소가 베타성과 목적의 특이성을 가져야 한다고 말합니다.
즉, 어떤 요소도 다른 요소의 책임을 공유하거나 관련 없는 책임을 포함해서는 안 됩니다.
const loginForm = document.getElementsByClassName('loginForm')[0];
const loginBtn = document.getElementById('loginBtn');
const 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();
현재 위의 코드를 보면 handleInput은 id와 pw의 값이 있는지 없는지 유효성 검사를 진행하고, 버튼의 스타일을 변경하는 로직이다.
handleInput의 기능을 관심사별로 분리해 보겠다.
const loginForm = document.getElementsByClassName('loginForm')[0];
const loginBtn = document.getElementById('loginBtn');
// 유효성 검사
const validateForm = () => {
const idValue = document.getElementById('id').value;
const pwValue = document.getElementById('pw').value;
const isIdValid = idValue.length > 1;
const isPasswordValid = pwValue.length > 1;
return isIdValid && isPasswordValid;
}
// 버튼 활성화,비화성화 스타일
const handleButtonActive = (isButtonActive) => {
loginBtn.disabled = !isButtonActive;
loginBtn.style.opacity = isButtonActive ? 1 : 0.3;
loginBtn.style.cursor = isButtonActive ? 'pointer' : 'default';
}
// validateForm, handleButtonActive 실행
const handleLoginInput = () => {
const isFormValid = validateForm();
handleButtonActive(isFormValid);
}
const init = () => {
loginForm.addEventListener('input', handleLoginInput);
};
init();
위처럼 하나의 함수가 하나의 기능을 담당하도록 구현하면, 유호성 검사 조건을 변경해야 한다면
'validateForm'를 수정해주면되고, 버튼 스타일을 변경해야된다면 'handleButtonActive'를 찾아 수정해주면 된다.
이처럼 관심사를 분리하면 변경해야 하는 상황이 발생했을때, 해당 부분을 담당하는 함수만 찾아서
코드를 변경해주면 되기 때문에 유지보수에도 편하고 간단하게 수정 할 수 있다.
2. Custom Hooks
커스텀 훅(Custom Hook)은 이름이 use로 시작하는 자바스크립트 함수입니다.
커스텀 훅을 사용하면 지금까지 컴포넌트 내부에 한 덩이로 결합하여 사용했던 State와 Effect를 분리하여 사용할 수 있습니다.
마치 여러 벽돌을 끼워 맞춰 건물을 만들듯이 React 컴포넌트를 여러 Hook을 조합하는 방식으로 개발할 수 있게 됩니다.
또한 로직을 독립적인 함수로 분리함으로써 하나의 로직을 여러 곳에서 반복적으로 재사용할 수 있게 됩니다.
분리 전
import React, { useState } from 'react';
const UserUI = () => {
const [isActive, setIsActive] = useState(false);
const handleToggle = () => {
setIsActive(prev => !prev);
};
return (
<>
<h1>현재 사용자의 상태입니다.</h1>
<span>{isActive ? '사용 중' : '사용 안 함'}</span>
<button onClick={handleToggle}>사용 상태 변경</button>
</>
);
};
export default UserStatus;
로직과 UI의 분리
import React, { useState } from 'react';
const useToggle = (initialValue = false) => {
const [state, setState] = useState(initialValue);
const handleToggle = () => {
setState((prev) => !prev);
};
return [state, handleToggle];
};
const UserUI = () => {
const [isActive, changeStatus] = useToggle();
return (
<>
<h1>현재 사용자의 상태입니다.</h1>
<span>{isActive ? '사용 중' : '사용 안 함'}</span>
<button onClick={changeStatus}>사용 상태 변경</button>
</>
);
};
export default UserStatus;
분리전
//Instructor.js
import axios from 'axios';
import React, { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import * as S from './Styled.Instructor';
import useInsturctor from './useInstructor';
const Instructor = () => {
const [instructorList, setInstructorList] = useState();
useEffect(() => {
const getInstructor = async () => {
const gets = await axios('data/instructors/instructors.json'); // 분리 할 부분
setInstructorList(gets.data);
};
getInstructor();
}, []);
return (
<>
<S.InstructorTitle>INSTRUCTORS</S.InstructorTitle>
<S.SubTitle>안무가</S.SubTitle>
<S.InstructorFlex>
{instructorList?.map(({ id, url, name }) => (
<Link to={`/instructorDetail/${id}`} key={id}>
<S.Square>
<S.ImgBox color={id}>
<S.Img src={url} alt={name} />
</S.ImgBox>
<S.TestBox>
<S.TestLine>{name}</S.TestLine>
<S.TestNoLine />
<S.TestLine>→</S.TestLine>
</S.TestBox>
</S.Square>
</Link>
))}
</S.InstructorFlex>
</>
);
};
export default Instructor;
출처 클론코딩 프로젝트
리턴 위에서 사용한 위 코드는 데이터를 get 하기위해 쓰이는 로직이다.
심지어 다른 파일에서도 이와 비슷한 로직을 많이 볼 수 있는데,
이를 Custom Hook으로 분리해서 관리 해보았다.
//Instructor.js
import { Link } from 'react-router-dom';
import * as S from './Styled.Instructor';
import useGetData from './useInstructor'; //분리한 로직 import
const Instructor = () => {
const instructorList = useInsturctor('data/instructors/instructors.json');
return (
<>
<S.InstructorTitle>INSTRUCTORS</S.InstructorTitle>
<S.SubTitle>안무가</S.SubTitle>
<S.InstructorFlex>
{instructorList?.map(({ id, url, name }) => (
<Link to={`/instructorDetail/${id}`} key={id}>
<S.Square>
<S.ImgBox color={id}>
<S.Img src={url} alt={name} />
</S.ImgBox>
<S.TestBox>
<S.TestLine>{name}</S.TestLine>
<S.TestNoLine />
<S.TestLine>→</S.TestLine>
</S.TestBox>
</S.Square>
</Link>
))}
</S.InstructorFlex>
</>
);
};
export default Instructor;
//useGetData
import { useEffect, useState } from 'react';
import axios from 'axios';
const useGetData = url => {
const [instructorList, setInstructorList] = useState();
useEffect(() => {
const getData = async () => {
const gets = await axios(url);
setInstructorList(gets.data);
};
getData();
}, [url]);
return useGetData;
};
export default useInsturctor;
useGetData는 데이터 통신에서 가져오기를 구현한 로직이다.
이 함수를 호출할때, 'useInsturctor('data/instructors/instructors.json')' 이렇게 url만 바꿔주면 다른 파일에서도 한줄로 8줄을 대체하는 효과를 얻을 수 있다.
매듭 짓기
React에서 Hook의 동작을 처리하는 내부적인 규칙과도 관련이 되어 있고, 공식적인 컨벤션이기 때문에 커스텀 훅을 작성하실 때는 꼭 use- 로 시작하는 이름을 지어주셔야 한다고 한다.
간단한 예시였지만, 이처럼 자주 중복되는 코드의 규칙을 찾아 커스텀 훅으로 분리하면,
코드가 길어 질수록 가독성, 유지보수에 도움이 되지않을까 싶어서 작성하게 되었다.