major.minor.patch
의 형태breaking change는 기존 코드와의 호환성을 깨뜨리는 변경을 의미합니다. 이로 인해 기존 코드가 올바르게 작동하지 않거나 컴파일되지 않을 수 있습니다. breaking change가 있는 경우, 해당 변경사항을 사용하려면 기존 코드를 수정해야 할 수도 있습니다.
간단한 예시를 통해 이해해보겠습니다. 가정해 봅시다. 당신은 패키지의 버전이 1.0.0이며, 이 패키지에는 add
라는 함수가 있습니다. 이 함수는 두 개의 숫자를 더해주는 역할을 합니다.
그런데 패키지 개발자가 새로운 버전인 2.0.0을 릴리스하고, add
함수를 변경했습니다. 이제 add
함수는 세 개의 숫자를 더해주는 기능을 가지고 있습니다. 이는 breaking change입니다.
기존 버전 1.0.0을 사용하던 코드에서는 add(2, 3)
과 같이 두 개의 숫자만 넘겨주면 되었지만, 새로운 버전 2.0.0에서는 add(2, 3, 4)
와 같이 세 개의 숫자를 모두 넘겨주어야 합니다. 그렇지 않으면 컴파일 에러가 발생하거나 예상치 못한 결과를 가져올 수 있습니다. 이로 인해 기존 코드가 올바르게 작동하지 않으므로, breaking change가 발생한 것입니다.
breaking change는 주로 다음과 같은 경우에 발생할 수 있습니다:
breaking change가 있는 업데이트를 수행할 때는 주의해야 하며, 업데이트 전에 변경 내용을 충분히 이해하고, 기존 코드와의 호환성을 확인하는 것이 중요합니다.
가정해 봅시다. 당신은 NPM을 사용하여 프로젝트의 의존성 관리를 하고 있습니다. 현재 사용 중인 패키지의 버전은 1.2.3입니다. 이 패키지가 어떤 변경 사항을 포함하고 있는지 파악하고자 합니다.
Major 버전 업데이트: major 버전은 주로 breaking change(하위 호환성이 없는 변경)이 포함된 경우에 올려집니다. 예를 들어, 만약 패키지의 버전이 2.0.0으로 업데이트 된다면, 이는 기존 버전과 호환되지 않는 큰 변경이 있었음을 의미합니다. 예를 들어, 패키지의 API가 완전히 변경되었거나 중요한 기능이 추가되었을 수 있습니다.
Minor 버전 업데이트: minor 버전은 주로 새로운 기능이 추가되었지만, breaking change는 없는 경우에 올려집니다. 예를 들어, 만약 패키지의 버전이 1.3.0으로 업데이트 된다면, 이는 기존 버전과 호환되는 새로운 기능이 추가되었음을 의미합니다. 이는 기존 API에 영향을 주지 않고 새로운 기능을 사용할 수 있게 됩니다.
Patch 버전 업데이트: patch 버전은 주로 버그 수정이나 기능에 영향을 주지 않는 작은 변경 사항이 있을 때 올려집니다. 예를 들어, 만약 패키지의 버전이 1.2.4로 업데이트 된다면, 이는 기존 버전과 호환되면서 버그가 수정되었음을 의미합니다. 버그 픽스나 작은 수정 사항은 patch 버전을 통해 배포됩니다.
간단한 예시로 설명하면, 버전 1.2.3의 패키지에서는 버그가 발견되어 이를 수정하고자 합니다. 이 경우, 패치된 버전은 1.2.4가 됩니다. 이는 breaking change가 없으며, 기존 버전과 호환되기 때문에 패치 버전으로 표기됩니다.
다음으로, 패키지에 새로운 기능을 추가하고자 합니다. 이러한 변경은 breaking change가 없기 때문에 기존 버전과 호환됩니다. 따라서, minor 버전을 올려주어야 합니다. 예를 들어, 기존 버전이 1.2.4라면, 새로운 기능이 추가된 버전은 1.3.0이 됩니다.
마지막으로, 기존 API를 완전히 변경하거나 breaking change가 있는 큰 변경을 하려고 합니다. 이 경우, major 버전을 올려주어야 합니다. 예를 들어, 기존 버전이 1.3.0이라면, breaking change가 포함된 변경 사항이 있으면 버전은 2.0.0으로 업데이트됩니다.
이런 식으로 시멘틱 버전 표기법을 사용하면, 개발자는 패키지의 변경 내용을 빠르게 파악할 수 있고, 업데이트가 기존 코드와 호환되는지 여부를 확인할 수 있습니다.
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.14.0",
"react-scripts": "5.0.1",
},
"node_modules/react-dom": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
"integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.0"
},
"peerDependencies": {
"react": "^18.2.0"
}
},
package.json과 package-lock.json은 둘 다 NPM 프로젝트에서 사용되는 파일입니다. 각각의 파일에는 프로젝트의 의존성과 관련된 정보가 포함되어 있습니다. 이 두 파일은 서로 다른 목적과 장점을 가지고 있습니다.
package.json
:
package-lock.json
:
요약하면, package.json은 프로젝트의 설정 정보와 의존성 범위를 정의하는 데 사용되며, package-lock.json은 의존성 트리의 구체적인 정보와 정확한 의존성 버전을 포함하는 데 사용됩니다. 두 파일은 함께 사용되어 프로젝트의 의존성을 관리하고, 일관된 환경을 유지할 수 있도록 도와줍니다.
npm i와 npm ci는 둘 다 NPM을 사용하여 프로젝트의 의존성을 설치하는 명령어입니다. 그러나 각각의 목적과 사용 시기가 다릅니다. 아래에서 각 명령어의 사용 시기를 설명하겠습니다:
npm i
:
npm ci
:
요약하면, npm i는 개발 중인 프로젝트에서 의존성을 설치하고 업데이트하는 데 사용되며, package.json에 정의된 의존성 범위를 고려합니다. 반면에, npm ci는 배포나 CI/CD 환경에서 정확한 의존성 트리를 구성하고, package-lock.json에 명시된 의존성 버전을 정확히 설치하는 데 사용됩니다.
package.json의 version 표기법인 Tilde(~)과 Caret(^)을 이해하는 것은 중요합니다. 아래에서 각각의 표기법에 대해 설명하겠습니다.
Tilde (~)
:
X.Y.Z
라고 가정해보겠습니다.~X.Y.Z
형식으로 사용할 때, 버전은 >=X.Y.Z
와 <X.(Y+1).0
사이의 범위에 해당합니다.~0.1
은 >=0.1.0
버전부터 <0.2.0
버전까지를 허용합니다.~0.1.1
은 >=0.1.1
버전부터 <0.2.0
버전까지를 허용합니다.~0
은 >=0.0.0
버전부터 <1.0.0
버전까지를 허용합니다.Caret (^)
:
X.Y.Z
라고 가정해보겠습니다.^X.Y.Z
형식으로 사용할 때, 버전은 >=X.Y.Z
와 <(X+1).0.0
사이의 범위에 해당합니다.^1.1.2
는 >=1.1.2
버전부터 <2.0.0
버전까지를 허용합니다.^1.0
은 >=1.0.0
버전부터 <2.0.0
버전까지를 허용합니다.^1
은 >=1.0.0
버전부터 <2.0.0
버전까지를 허용합니다.^0.1.2
는 >=0.1.2
버전부터 <0.2.0
버전까지를 허용합니다.^0.2
는 >=0.2.0
버전부터 <0.3.0
버전까지를 허용합니다.^0.0.2
는 0.0.2 버전만
허용합니다.이러한 표기법을 사용하면 개발자는 의존성 패키지의 업데이트를 통제할 수 있습니다.
Semantic Versioning에서는 버전 번호의 세 가지 요소를 사용합니다: major.minor.patch
. 이들 요소의 의미와 업데이트의 특성을 설명하겠습니다.
Semantic Versioning은 이러한 업데이트 방식을 통해 의존성 패키지의 버전을 관리하고 호환성을 유지합니다. 일반적으로 Major 업데이트 이전의 버전 번호를 사용하는 경우 호환성이 유지되며, Minor와 Patch 업데이트는 보다 작은 변화를 나타내므로 호환성 이슈가 적은 업데이트입니다.
따라서 Semantic Versioning에서는 Minor 버전까지의 업데이트에서는 호환성 이슈가 발생하지 않는 것을 기대하고 있습니다. 그러나 보안 문제 해결과 같이 크리티컬한 이슈를 해결하기 위한 버그 수정은 Patch 업데이트로 처리될 수 있습니다. 따라서 bug fix 등의 업데이트는 주로 Patch 업데이트로 처리되며, 버전 표기법을 이용하여 업데이트의 허용 범위를 명시합니다.
이를 통해 의존성 패키지의 업데이트를 조절하고 필요한 보안 수정 등을 수행할 수 있습니다.
const handleClickSave = async () => {
if (modifiedTodo === '') {
alert('입력 값이 없습니다.');
return;
}
// 기존과 값이 변경된 경우에만 api 호출 => 주석은 코드를 설명하는게 아니다. (유지보수에 문제)
if (modifiedTodo !== todo) {
await updateTodo({ id, todo: modifiedTodo, isCompleted });
}
setIsOnModify(false);
};
function calculateTax(income) {
// 소득이 특정 기준 이상인 경우 추가 세금이 부과됩니다.
if (income > 50000) {
const additionalTax = income * 0.1;
return income + additionalTax;
}
return income;
}
const handleCheck = async () => {
updateTodo({ id, todo, isCompleted: !isCompleted });
};
const toggleComplete = async () => {
updateTodo({ id, todo, isCompleted: !isCompleted });
};
const [form, setForm] = useState<IAuthType>({
email: '',
password: '',
});
const [isValid, setIsValid] = useState<IAuthValidType>({
isEmail: false,
isPassword: false,
});
const [disabled, setDisabled] = useState(true);
useEffect(() => {
setDisabled(!(isValid.isEmail && isValid.isPassword));
}, [isValid.isEmail, isValid.isPassword]);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
event.preventDefault();
const current = event.target.value;
switch (event.target.type) {
case 'email':
const regex = /\S+@\S+\.\S+/; // eslint-disable-line
setIsValid(prev => ({
...prev,
isEmail: !regex.test(current) ? false : true,
}));
setForm(prev => ({ ...prev, email: current }));
break;
case 'password':
setIsValid(prev => ({
...prev,
isPassword: current.length < 8 ? false : true,
}));
setForm(prev => ({ ...prev, password: current }));
break;
}
};
const [form, setForm] = useState<IAuthType>({
email: '',
password: '',
});
const isValid = {
isEmail: /\S+@\S+\.\S+/.test(form.email),
isPassword: form.password.length < 8,
};
const isDisabled = !(isValid.isEmail && isValid.isPassword);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
event.preventDefault();
const current = event.target.value;
switch (event.target.type) {
case 'email':
setForm(prev => ({ ...prev, email: current }));
break;
case 'password':
setForm(prev => ({ ...prev, password: current }));
break;
}
};
const Login = () => {
return <AuthContainer title="로그인" dataTestid="signin-button" />;
};
const Signup = () => {
return <AuthContainer title="회원가입" dataTestid="signup-button" />;
};
const AuthContainer = ({ title, dataTestid }: PropsType) => {
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
try {
setDisabled(true);
if (location === '/signup') {
await SIGNUP({
email: form.email,
password: form.password,
});
alert('회원가입이 성공하였습니다!\n로그인을 시도해주세요.');
navigate('/signin');
}
if (location === '/signin') {
await SIGNIN({
email: form.email,
password: form.password,
});
alert('환영합니다!');
navigate('/todo');
}
} catch (error: any) {
alert(
location === '/signup'
? error.response.data.message
: location === '/signin'
? '이메일이나 비밀번호를 다시 확인해주세요.'
: ''
);
}
};
return (
<AuthContainerPresenter
title={title}
data={form}
isValid={isValid}
dataTestid={dataTestid}
disabled={disabled}
onChange={handleChange}
onSubmit={handleSubmit}
/>
);
};
const Login = () => {
const navigate = useNavigate();
const signin = async (email, password) => {
try {
await SIGNIN({
email,
password,
});
alert('환영합니다!');
navigate('/todo');
} catch (err) {
alert('이메일이나 비밀번호를 다시 확인해주세요');
}
};
return (
<AuthContainer
title="로그인"
dataTestid="signin-button"
onSubmit={signin}
/>
);
};
const Signup = () => {
const navigate = useNavigate();
const signup = async (email, password) => {
try {
await SIGNUP({
email,
password,
});
alert('회원가입이 성공하였습니다!\n로그인을 시도해주세요.');
navigate('/signin');
} catch (err) {
alert(err.response.data.message);
}
};
return (
<AuthContainer
title="회원가입"
dataTestid="signup-button"
onSubmit={signup}
/>
);
};
const AuthContainer = ({ title, dataTestid, onSubmit }: PropsType) => {
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setDisabled(true);
onSubmit(form.email, form.password);
};
return (
<AuthContainerPresenter
title={title}
data={form}
isValid={isValid}
dataTestid={dataTestid}
disabled={disabled}
onChange={handleChange}
onSubmit={handleSubmit}
/>
);
};
export default AuthContainer;
const [auth, setAuth] = useState({
email: '',
pw: '',
});
const emailRegex = /@+/;
const pwRegex = /.{8,}/;
//const isValidEmail = useMemo(() => emailRegex.test(auth.email), [auth.email]);
//const isValidPw = useMemo(() => pwRegex.test(auth.pw), [auth.pw]);
//const isValid = isValidEmail && isValidPw;
const isValidEmail = emailRegex.test(auth.email);
const isValidPw = pwRegex.test(auth.pw);
const isValid = isValidEmail && isValidPw;
import styled from 'styled-components';
import { Link } from 'react-router-dom';
import { validate } from '../../utils';
import { useAuth } from '../../hooks';
import { Input, Button, InvisibleLabel } from '..';
import { PATH, DATATESTID } from '../../constants';
const formConfig = {
signin: {
formTypeText: '로그인',
dataTestId: DATATESTID.SIGN_IN_BTN,
link: PATH.SIGN_UP,
linkText: '처음 오셨나요?',
},
signup: {
formTypeText: '회원가입',
dataTestId: DATATESTID.SIGN_UP_BTN,
link: PATH.SIGN_IN,
linkText: '이미 회원이신가요?',
},
};
const Form = styled.form`
display: flex;
justify-content: space-between;
flex-direction: column;
`;
const Warning = styled.span`
font-size: 14px;
color: red;
padding-left: 3px;
`;
const SubmitButton = styled(Button)`
margin: 6px 0 0 0;
`;
const ChangeFormButton = styled(Button)`
margin: 5px 0px;
background-color: white;
color: Dodgerblue;
`;
const SignInput = styled(Input)`
margin: 12px 0 6px 0;
`;
const SignForm = ({ formType }) => {
const { formValues, error, handleValueChange, handleSignInSubmit, handleSignUpSubmit } = useAuth();
const [emailValid, passwordValid] = validate(formValues);
const allValid = emailValid && passwordValid;
const signIn = formType === 'signin';
return (
<>
<h1>{formConfig[formType].formTypeText}</h1>
<Form onSubmit={signIn ? handleSignInSubmit : handleSignUpSubmit}>
<InvisibleLabel htmlFor="email">email input</InvisibleLabel>
<SignInput
id="email"
name="email"
value={formValues.email}
onChange={handleValueChange}
data-testid={DATATESTID.EMAIL_INPUT}
placeholder="이메일을 입력해주세요."
inValid={formValues.email && !emailValid}
/>
{formValues.email && !emailValid && <Warning>아이디는 @를 포함해야합니다.</Warning>}
<InvisibleLabel htmlFor="password">password input</InvisibleLabel>
<SignInput
id="password"
name="password"
type="password"
value={formValues.password}
onChange={handleValueChange}
data-testid={DATATESTID.PASSWORD_INPUT}
placeholder="비밀번호를 입력해주세요."
inValid={formValues.password && !passwordValid}
/>
{formValues.password && !passwordValid && <Warning>비밀번호는 8자 이상이여야 합니다.</Warning>}
{error && <Warning>{error.response.data.message}</Warning>}
<SubmitButton type="submit" data-testid={formConfig[formType].dataTestId} disabled={!allValid}>
{formConfig[formType].formTypeText}
</SubmitButton>
</Form>
<Link to={formConfig[formType].link}>
<ChangeFormButton>{formConfig[formType].linkText}</ChangeFormButton>
</Link>
</>
);
};
export default SignForm;
import styled from 'styled-components';
import { Link } from 'react-router-dom';
import { validate } from '../../utils';
import { useAuth } from '../../hooks';
import { Input, Button, InvisibleLabel } from '..';
import { PATH, DATATESTID } from '../../constants';
const SignForm = ({ formType }) => {
const { formValues, error, handleValueChange, handleSignInSubmit, handleSignUpSubmit } = useAuth();
const [emailValid, passwordValid] = validate(formValues);
const allValid = emailValid && passwordValid;
const signIn = formType === 'signin';
return (
<>
<h1>{formConfig[formType].formTypeText}</h1>
<Form onSubmit={signIn ? handleSignInSubmit : handleSignUpSubmit}>
<InvisibleLabel htmlFor="email">email input</InvisibleLabel>
<SignInput
id="email"
name="email"
value={formValues.email}
onChange={handleValueChange}
data-testid={DATATESTID.EMAIL_INPUT}
placeholder="이메일을 입력해주세요."
inValid={formValues.email && !emailValid}
/>
{formValues.email && !emailValid && <Warning>아이디는 @를 포함해야합니다.</Warning>}
<InvisibleLabel htmlFor="password">password input</InvisibleLabel>
<SignInput
id="password"
name="password"
type="password"
value={formValues.password}
onChange={handleValueChange}
data-testid={DATATESTID.PASSWORD_INPUT}
placeholder="비밀번호를 입력해주세요."
inValid={formValues.password && !passwordValid}
/>
{formValues.password && !passwordValid && <Warning>비밀번호는 8자 이상이여야 합니다.</Warning>}
{error && <Warning>{error.response.data.message}</Warning>}
<SubmitButton type="submit" data-testid={formConfig[formType].dataTestId} disabled={!allValid}>
{formConfig[formType].formTypeText}
</SubmitButton>
</Form>
<Link to={formConfig[formType].link}>
<ChangeFormButton>{formConfig[formType].linkText}</ChangeFormButton>
</Link>
</>
);
};
const formConfig = {
signin: {
formTypeText: '로그인',
dataTestId: DATATESTID.SIGN_IN_BTN,
link: PATH.SIGN_UP,
linkText: '처음 오셨나요?',
},
signup: {
formTypeText: '회원가입',
dataTestId: DATATESTID.SIGN_UP_BTN,
link: PATH.SIGN_IN,
linkText: '이미 회원이신가요?',
},
};
const Form = styled.form`
display: flex;
justify-content: space-between;
flex-direction: column;
`;
const Warning = styled.span`
font-size: 14px;
color: red;
padding-left: 3px;
`;
const SubmitButton = styled(Button)`
margin: 6px 0 0 0;
`;
const ChangeFormButton = styled(Button)`
margin: 5px 0px;
background-color: white;
color: Dodgerblue;
`;
const SignInput = styled(Input)`
margin: 12px 0 6px 0;
`;
export default SignForm;
BAD 코드와 GOOD 코드의 차이점은 다음과 같습니다:
BAD 코드:
GOOD 코드:
GOOD 코드는 코드의 가독성과 유지 보수성을 개선하기 위해 관련 코드들을 논리적인 블록으로 그룹화하고 구조화했습니다. 스타일 관련 코드를 컴포넌트 아래에 위치시킴으로써 컴포넌트의 구조를 더욱 명확하게 표현하고, 코드의 재사용성을 높일 수 있습니다. 또한 formConfig 객체도 관련된 코드 아래에 위치시킴으로써 읽기 쉽고 이해하기 쉬운 구조를 갖추게 되었습니다.