홍플릭스가 단순 과제 개념이라 선택사항이었다면 그 다음 프로젝트인 Web IDE는 필수였다.
그러니까 원래 팀끼리 하는 과제 1번이 SNS 클론, 2번이 넷플릭스 클론, 3번이 쇼핑몰, 4번이 Web IDE 였던 것 같은데, 커리큘럼이 바뀌면서 다음 코스였던 쇼핑몰은 빠지게 되고 (경쟁력 없는 포폴이라고 생각하신듯?) 4번이 필수 과제가 되었다.
우리 팀은 Web IDE에는 그다지 니즈가 없었어서... 내키지 않는 프로젝트라고 생각했다.
그런데 반드시 붙여야 하는 과제가 명확해서 나름대로 성과가 있었다. (비밀번호 재설정이나 채팅 기능이 필수였는데 이전에 프로필 수정 부분을 구현하고 싶었어서 잘됐다 싶었다.) 채팅이나 Web IDE 자체는 어떻게 구현해야 할지; 도무지 감이 잡히지 않아 난관이긴 했다.
그래도 어차저차 힘을 합쳐 제법 그럴싸하게 구현했고 프로젝트 기획안은 전체 1등, 프로젝트 성과물은 2등을 차지했다. 그리고 같은 기수 플레이어 분들이 응원의 메세지를 많이 남겨주셔서 뿌듯했던 프로젝트였다. 😌
기획안
우리 팀은 '코딩 테스트를 위한 페어 프로그래밍 코드 편집기 근데 이제 타이머를 곁들인'을 테마로 했다. 우리 팀은 알고리즘 스터디에서 시작하면서 서로의 코드를 피드백해줄 때 페어 프로그래밍에 대한 니즈가 생겼고, 좀 더 원활하게 코딩 테스트를 대비하고자 시간을 잴 수 있다면 좋겠다고 생각해서 별도로 타이머 기능을 추가했다.
구름 측에서 원했던 건 동시 편집인 CRDT가 되는, 정말 구름 컨테이너를 클론하는 느낌이었을 것 같은데 4주라는 기간동안 그건 너무 스케일이 크고 학습할 양이 많아서 어려울 것 같았다. 그래서 프로그래머스처럼 문제를 풀 수 있는 에디터가 있는 쪽으로 방향을 틀었고, 문제 DB도 등록해서 유저가 문제를 풀 수 있도록 하는 웹 페이지를 만들어보았다.
원래는 마크다운 에디터인 toast UI를 추가하려고 했으나 구체적으로 어떻게 도입해야 할지 몰라 패스했는데 오히려 기획안에서 덜어내서 다행이었다. 우리는 기획안에서 기획한 것들은 모두 구현하려고 노력했고 실제로 구현했기 때문이다.
기술 스택
FE: React, Tailwind CSS
BE: Spring boot, Redis, Mysql
DevOps: Docker, Jenkins, AWS
Communication: Github, Slack, Discord, Notion
그외로 react-hook-form, Monaco Editor, StompJS, SockJS를 사용했다.
TO DO
- 메인 페이지
- 로그인/회원가입
- /question (문제 목록 페이지)
- 페이지네이션
- 채팅 기능
- /mypage (유저 프로필)
- 프로필 이미지 수정
- 유저 이름, 비밀번호 재설정
- 탈퇴하기
- 다크 모드
이번에는 웹서버를 메인(내가 맡은 부분) + 에디터(갱갱규님이 맡은 부분)으로 나눠서 작업을 해서 merge 같은 스트레스를 받지 않아서 좋았다. 다만 완전히 분업의 개념이라 서로의 코드를 리뷰할 수 없었다는 점이 가장 큰 아쉬움이었다. 아무튼 그래서 나는 전반적으로 유저와 관련된 부분을 맡았고, 갱갱규님은 문제를 직접 풀 수 있는 부분(코드 입력, 코드 실행, 코드 불러오기, 코드 컴파일 및 타이머)을 맡아주셨다.
메인 페이지
메인 페이지에 접속하고 '문제 풀이 시작' 버튼을 누르면 문제 목록 페이지(/question)으로 라우팅된다.
원래는 이 때부터 로그인 여부를 확인해 로그인이 되지 않으면 /login으로 라우팅을 시키려고 했는데, 이렇게 하면 유저 입장에서는 이게 무슨 서비스인지도 모르는데 가입을 하라고 하니 이탈률이 높아질 것이라고 판단했다. 따라서 문제 목록까지는 접근할 수 있도록 하고 문제를 풀기 위해 문제를 클릭하거나 헤더의 로그인을 클릭할 때 로그인을 하도록 유도하는 방향으로 플로우를 수정했다.
개발/운영 측에서는 회원가입이나 로그인이 유저를 특정 지을 수 있어 간편한 도구가 되지만 유저 입장에서는 이런 게 귀찮고, 이탈을 야기한다는 점을 깨닫게 된 계기였다. 나만해도 사실 유저일 때 그랬는데 ㅎㅎ; 개발/운영 측이 되어 보니 또 다른 관점에서 생각하게 되었다.
위에서 말했다시피 문제를 풀기 위해 문제를 클릭하거나, 상단 헤더의 로그인 버튼을 누르면 로그인을 할 수 있다.
로그인
이번에 로그인/회원가입은 react-hook-form이라는 라이브러리를 사용해 구현했다.
HongsamSNS에서 로그인/회원가입을 구현할 때는 일일이 조건을 확인하다 보니 코드도 길어지고 가독성도 좋지 않았다는 문제점이 있어 존안 코치님께 피드백을 받은 적이 있었는데, 이 라이브러리를 추천해주셨다. 레퍼런스도 많아 사용하기 쉽고, 개발자 입장에서는 코드가 간결하고 라이브로 유효성 검사를 할 수 있는 점, 유저 입장에서는 그로 인해 유저 경험이 개선된다는 점에서 해당 라이브러리를 선택해 구현해보았다.
아무 것도 입력하지 않고 로그인 버튼을 누르면 이 칸을 입력해달라는 빨간 경고 메세지가 뜬다.
만약 아이디가 틀렸다면 아이디에 focus를 주고 '아이디를 확인해주세요' 같이 개별적인 alert을 띄우자고 백엔드 노루스름님께 말씀드렸는데, 백엔드 입장에서는 보안에 좋지 않은 것 같다는 의견을 주셨다. 이 점으로는 생각해보지 못해서, 유저 경험 vs 보안으로 고민했을 때는 운영 입장에서 후자를 선택하는 게 맞다 싶어 로그인이 실패되면 (아이디가 틀리든 비밀번호가 틀리든 아예 가입을 안 했든) 무조건 '아이디 또는 비밀번호가 맞지 않다'라는 alert을 띄우고 focus는 별도로 주지 않는 것으로 합의했다.
return (
<div className={styles.Login}>
<div className={styles.subtitle}>Step for Developer</div>
<div className={styles.title}>Hongsam IDE</div>
<form className={styles.form} onSubmit={handleSubmit(onLogin)}>
<label>ID</label>
<input
name="email"
type="email"
autoComplete="off"
placeholder="아이디를 입력하세요."
{...register('email', {
required: true,
})}
/>
{errors.email && errors.email.type === 'required' && (
<p>이 칸을 입력해주세요.</p>
)}
<label>Password</label>
<input
name="password"
type="password"
autoComplete="off"
placeholder="비밀번호를 입력하세요."
{...register('password', {
required: true,
})}
/>
{errors.password && errors.password.type === 'required' && (
<p>이 칸을 입력해주세요.</p>
)}
<Link to={'/signup'}>Sign up</Link>
<button className={styles.loginBtn} type="submit">
Login
</button>
</form>
</div>
);
로그인 버튼의 우측 상단에 있는 sign up을 클릭하면 회원가입 페이지로 라우팅된다.
회원가입
회원가입 시 필요한 정보는 ID, 비밀번호(비밀번호 확인), 유저의 이름이다.
ID는 이메일 형식 (input의 type을 email로)
비밀번호는 영문+숫자+특수문자 조합의 7~15자,
비밀번호 확인은 비밀번호와 같게 입력했는지 재확인,
이름은 10자 이내라는 조건을 설정했다.
이메일 형식이 올바르지 않으면 '아이디 형식이 올바르지 않습니다'라는 빨간 경고 메세지가 바로 뜬다. 이렇게 라이브로 유효성 검사의 결과를 알려주는 것을 전부터 하고 싶었는데 매우 간단해서 좋았다.
비밀번호도 동일하게 설정한 영문+숫자+특수문자의 7~15자가 아니면 경고가 뜬다.
이메일 중복 확인을 하지 않고 제출을 하려고 하면 alert이 뜬다.
react-hook-form 자체에서 비동기 작업을 할 수 있을 거라고 FE 멘토님이 말씀해주셨는데 해당 기능에 대해 찾지 못해서 일단은 수작업으로 진행했다.
이메일의 중복 체크는 POST 요청으로 서버에 유저가 입력한 이메일을 전달하는 방식으로 진행했다. 따라서 이미 있는 정보라면 사용할 수 없는 이메일이라는 alert이 뜬다.
여기서 어떤 버그를 만났었다. 중복이라는 얼럿을 확인하고도 정이메일을 수정하지 않고 회원 가입 버튼을 누르면 아무 반응이 없다가, 다시 한 번 중복 확인 버튼을 누르면 중복 확인에 대한 alert(중복된 이메일이다)과 회원 가입에 대한 alert(이메일 확인해라)이 각각 1번씩 총 2번 뜨는 것이었다.
내가 원하는 것은 중복 이메일의 경우 회원 가입 버튼을 몇 번을 누르든 사용할 수 없는 이메일이라는 alert이 뜨게 하는 것인데... 왜 이러는 건지 궁금해 FE 멘토님께 여쭤보았다.
이유는 아래와 같다.
현재 HTML 태그에 form으로 구성되어 있고, 여기에 중복 확인과 회원 가입이라는 2개의 버튼이 존재하는데 버튼의 속성을 따로 정해주지 않으면 submit으로 지정되어 form의 onSubmit을 발동시키게 된다. 즉 버튼을 클릭했을 때 버튼 클릭 이벤트가 발생하고 form의 onSubmit이 발생하기 때문에 이러한 버그가 발생하는 것이었다. 따라서 해결책은 버튼에 type=button을 주면 된다.
이벤트 버블링과 캡쳐링에 대해 공부해보라고 조언해주셨고, preventDefault, stopPropagation이라는 메소드를 같이 찾아보라고 추천해주셨다. 아직 HTML에 대해서도 많이 모르는구나 싶었다. 🥲
중복 이메일에 1을 더해 다시 체크하니 정상적으로 사용 가능하다는 alert이 떴다.
캡쳐에서는 티가 나지 않지만, input에을 다시 입력해야 하는 경우에는 해당 input에 focus를 줬다.
이런 폼을 만들 때 내가 습관처럼 챙기는 부분은 hover, cursor pointer, focus 이렇게 3가지다.
당장 중요한 건 아니더라도 뭔가 이 3가지는 손가락이 호다다닥 챙기게 되는 것 같다 ㅎㅎ;
회원가입을 완료하면 상단 헤더에 유저의 이름과 서버에서 설정한 기본 프로필 사진이 노출된다.
회원가입 쪽은 input이 4개라서 코드가 조금 길다.
멘토님이 공통되는 코드는 빼도 좋겠다고 피드백을 주셨는데(문제가 된다기보다 내가 클린코드, 코드의 재사용성 면에서 피드백을 원했다.) 이번에는 건드렸다가 망할까봐 뒀고 수료하고 조금 정리를 해보거나 다른 프로젝트를 새로 시작할 때 사용해보려고 한다.
return (
<div className={styles.Signup}>
<div className={styles.subtitle}>Step for Developer</div>
<div className={styles.title}>Hongsam IDE</div>
<form className={styles.form} onSubmit={handleSubmit(onSignup)}>
<label>ID</label>
<div className={styles.id}>
<input
name="email"
type="text"
autoComplete="off"
placeholder="이메일 형식으로 입력해주세요."
{...register('email', {
required: true,
pattern: /^\S+@\S+$/i,
})}
/>
<button
className={styles.confirmIdBtn}
onClick={confirmID}
type="button"
>
중복 확인
</button>
</div>
{errors.email && errors.email.type === 'required' && (
<p>이 칸을 입력해주세요.</p>
)}
{errors.email && errors.email.type === 'pattern' && (
<p>아이디 형식이 올바르지 않습니다.</p>
)}
<label>Password</label>
<input
name="password"
type="password"
autoComplete="off"
placeholder="영문+숫자+특수문자 조합의 7~15자로 입력해주세요."
{...register('password', {
required: true,
pattern:
/^(?=.*[a-zA-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{7,15}$/i,
})}
/>
{errors.password && errors.password.type === 'required' && (
<p>이 칸을 입력해주세요.</p>
)}
{errors.password && errors.password.type === 'pattern' && (
<p>비밀번호 형식이 올바르지 않습니다.</p>
)}
<label>Confirm Password</label>
<input
name="confirm"
type="password"
autoComplete="off"
placeholder="비밀번호를 다시 입력해주세요."
{...register('confirm', {
required: true,
validate: (value) => value === passwordInputRef.current,
})}
/>
{errors.confirm && errors.confirm.type === 'required' && (
<p>이 칸을 입력해주세요.</p>
)}
{errors.confirm && errors.confirm.type === 'validate' && (
<p>비밀번호가 일치하지 않습니다.</p>
)}
<label>Name</label>
<input
name="username"
autoComplete="off"
placeholder="이름을 10자 이내로 입력해주세요."
{...register('username', {
required: true,
maxLength: 10,
})}
/>
{errors.username && errors.username.type === 'required' && (
<p>이 칸을 입력해주세요.</p>
)}
{errors.username && errors.username.type === 'maxLength' && (
<p>10자 이내로 입력해주세요.</p>
)}
<button className={styles.submitBtn} type="submit">
Sign up
</button>
</form>
</div>
);
결과물
로그인/회원가입 쪽을 구현하면서 유저의 입장과 운영진의 입장 모두를 이해하게 됐다.
또 유저 경험과 보안의 관계에 대해서도 생각해보게 되는 재미있는 경험이었다.
참 그리고 이번에는 프레임워크나 CSS 라이브러리를 쓰지 않았다.
그냥 CSS module로 했는데 얘네의 차이점에 대해서도 공부해봐야겠다;
react-hook-form은 많이 사용하는 라이브러리 & 매우 편하기 때문에 먼저 바닐라로 구현할 수 있다면 이후에는 이 라이브러리를 써서 효율성을 높여보는 것도 좋은 방안이라는 생각이 들었다. 아래 영상은 내가 참고한 존안 코치님의 영상인데 2년 전이라 지금은 조금 다른 부분들이 있다! (이걸 몰라서 헤맸음;) 댓글에 어떻게 업데이트 되었는지 다 알려주기 때문에 댓글을 꼭 확인할 것! 정확히는 공식 문서가 제일 좋겠지만!