5. 토큰 및 채점 기준 설정하기
채점 기준은 어떻게 만들어야 좋을까?
채점 서비스에서 채점하려면 그 기준이 반드시 필요하다.
따라서 프론트엔드 쪽에서 어떻게 채점 기준을 추가하고 세분화할 수 있는지 고민했다.
파일로 받기? 직접 입력하기?
채점 기준을 파일로 받는다면 사실 프론트엔드 쪽에서는 너무 간단하다.
백엔드 쪽으로 해당 파일만 그대로 넘겨주면 되니까.
하지만 파일 방식은 형식이 일정해야 하고, 보기에도 불편했다.
따라서 웹에서 채점 기준을 직접 입력하는 것이 훨씬 직관적일 것 같아 이 방식으로 구현할 것이다.
클래스는 어떻게 하지?
클래스는 (만약 수업에서 사용한다면) 각 과제 또는 실습 단위로 구성하려고 했다.
간단히 말해서 자바 프로그래밍 수업에서 과제가 5개만 나온다면 교수님은 5개의 클래스만 만드시면 되도록!
가장 오랫동안 고민한 것은 로그인 기능이었다.
- 구글 로그인 API와 같이 라이브러리를 사용해서 로그인을 구현해서 해당 교수님의 클래스로 Join하는 방식을 구현할 것인지,
- 로그인 없이 특정 인증 방법으로 클래스에 Join할 것인지.
각 루트는 장단점이 확실했다.
로그인을 구현한다면,
일단 학생들의 로그인 정보를 어떻게든 확인할 수 있으므로, 치팅이나 고의로 시스템을 훼손하는 행위 등은 거의 없을 것이다.
클래스 개설 및 입장이 굉장히 직관적일 것이다. (과제1, 과제2, … 이렇게 찾아서 들어갈 수도 있을 것)
로그인 API를 사용하는 것은 어렵지 않았으나, 세션 유지와 개인정보 이슈 (사용자 정보가 들어가 있으니까)가 발생한다.
로그인을 구현하지 않는다면,
위 로그인의 장점이 모두 단점으로 바뀐다😢
사용자의 정보를 일체 수집하지 않으므로 개인정보 이슈는 없다.
로그인을 대신할 인증 방식을 고민해봐야 하며, 상대적으로 구현이 편할 수 있다.
로그인 방식이 굉장히 보편적이고 클래스 분류, 입장 기능이 강력할 것 같아 이쪽으로 가닥을 잡고 구현하고 있었으나,
개인정보를 어떻게 관리할 것인지에 대한 피드백을 받았고 세션 유지도 구현 당시에는 어려워서 로그인 방식을 폐기했다.
그럼 어떻게 클래스룸을 확인하고 인증할 것인가?
토큰 인증 방식 구현하기
거창한 방식인 것 같지만 굉장히 간단하다.
클래스룸을 개설할 때 문자열 + 숫자를 조합한 난수 문자열(토큰)을 생성한다.
채점자가 채점 기준까지 모두 설정하면 토큰을 비롯한 채점 기준표를 백엔드로 전송한다.
클래스룸에 들어가고자 하는 학생들은 미리 배포된 토큰을 이용하며, 입력된 토큰이 실제 데이터베이스에 저장되어 있는지 확인한다.
그럼, 난수부터 만들어 보자!
// file: 'EntranceInstructor.tsx'
...
const handleGenerate = () => {
let random = Math.random().toString(36).substr(2, 11);
let irandom = Math.random().toString(36).substr(2, 6);
setInfo({ ...info, token: random, itoken: irandom });
}
토큰은 학생들에게 배포되는 random
토큰, 일종의 관리자 페이지를 위한 itoken
을 각각 만들어 주었다.
그리고 이를 백엔드에 보낼 간단한 정보를 저장하는 info
에 React hook을 사용해 저장한다.
여기서도 ...info
를 사용해 spreading operator
로 나머지 배열들을 그대로 저장하고,
itoken
, token
만 업데이트해준다.
그 다음, 개설할 클래스 이름과 채점자 이름을 입력하고,
채점 결과를 보여줄 것인지 여부를 체크하는 변수도 추가한다.
(채점 결과는 점수, 채점 기준 중 어느 것을 틀렸는지에 대한 간단한 메시지를 줄 예정인데, 아예 보여주지 않는 것도 필요할 것 같아 옵션으로 넣었다.)
// file: 'EntranceInstructor.tsx'
return (
...
{open &&
<Dialog
open={open}
onClose={handleClose}
aria-labelledby="instructor-token-generator"
aria-describedby="instructor-token-description"
disableBackdropClick={true}
disableEscapeKeyDown={true}
maxWidth="sm"
scroll='paper'
>
<DialogTitle id="instructor-token-title">
다이얼로그 제목
</DialogTitle>
<DialogContent>
<DialogContentText>
다이얼로그 내용
</DialogContentText>
<Grid container spacing={2}>
<Grid xs={12} item>
<FormControl margin="normal">
<TextField
value={info.className || ""}
variant="outlined"
label="클래스 네임"
size="medium"
className={classes.dialogText}
onChange={handleInfoChange("className")}
/>
<TextField
value={info.instructor || ""}
variant="outlined"
label="개설자 네임"
size="medium"
className={classes.dialogText}
onChange={handleInfoChange("instructor")}
/>
</FormControl>
</Grid>
<Grid xs={12} item>
<Button variant="contained"
color="primary"
onClick={handleGenerate}
disabled={info.className.length < 3 || info.instructor.length < 3}>
생성하기
</Button>
</Grid>
</Grid>
...
채점 기준을 입력하기 위해 링크를 누르면 Dialog
가 뜨도록 했다.
페이지를 이동해 작성하는 것보다 코드를 구성하기 편하기도 하고, 데이터를 모아두는 것도 보다 쉽다.
Material-UI의 Dialog
컴포넌트와 Grid
컴포넌트를 임포트해 사용했다.
DialogContent
컴포넌트에 다이얼로그 내부 내용을 지정할 수 있으며,
여기에 클래스 이름과 개설자 이름을 넣고 토큰 생성 버튼을 누르면 출력하도록 했다.
🔥 TextField
컴포넌트와 종일 씨름한 순간이다.
특히,
...
<TextField
value={info.className || ""}
variant="outlined"
label="클래스 네임"
size="medium"
className={classes.dialogText}
onChange={handleInfoChange("className")}
/>
...
이 부분이었는데, 각 TextField
에 커서를 놓고 입력해도 입력이 안 되거나,
입력이 들어가도 두 TextField
에 같은 내용이 찍혀버리는 상황이 발생했다.
이는 TextField
의 value
와 onChange
props를 잘 구성했어야 했다.
초기 value
에 지정된 변수의 값이 없으면 ""
을 넣고, 뭔가 입력된다 싶으면 onChange
로 핸들링해줬다.
// file: 'EntranceInstructor.tsx'
interface InfoProps {
className: string;
instructor: string;
token: string;
itoken: string;
direct: boolean;
}
...
const handleInfoChange = (prop: keyof InfoProps) => (e: React.ChangeEvent<HTMLInputElement>) => {
setInfo({ ...info, [prop]: e.target.value });
}
...
핸들링 함수는 위와 같으며, 핸들링 함수를 호출할 때 같이 넘겨준 매개변수로 해당 TextField
에 맞는 값을 업데이트한다.
인터페이스는 InfoProps
와 같이 해당 다이얼로그 페이지에서 받을 정보의 타입을 지정했다.
onChange
는 이벤트가 발생하고, 입력 시 발생하는 이벤트는 React에서 React.ChangeEvent<HTMLInputElement>
로 타입이 정의되어 있다.
저렇게 TextField
의 onChange
핸들링 함수 호출 시 매개변수를 다르게 주고, hook으로 변경하면 아주 잘 동작한다!
버튼을 누르면 handleGenerate
핸들링 함수를 호출하여 토큰을 생성한다.
최소한의 제약조건을 위하여 클래스 이름과 개설자 이름의 길이가 3을 넘지 않으면 비활성화했다.
// file: 'EntranceInstructor.tsx'
...
<Typographic variant="h3" color="inherit">
<Typographic variant="caption" color="inherit">
클래스 토큰 <br />
</Typographic>
{info.token}
<br />
<Typographic variant="caption" color="inherit">
채점용 토큰 <br />
</Typographic>
{info.itoken}
</Typographic>
<FormControlLabel
control={
<Checkbox checked={info.direct}
onChange={handleChecked}
name="directFeedback"
color="primary" />}
label="즉시 피드백 제공 활성화"
/>
</DialogContent>
...
그 다음, 버튼을 눌러 생성했다면 토큰을 보여줘야 한다.
처음 info
의 토큰들은 비어 있는 문자열로 초기화되어 있으므로 아무것도 출력되지 않지만
handleGenerate
핸들링 함수로 생성되면 즉시 출력한다.
피드백을 바로 줄 것인지 결정하는 Checkbox
컴포넌트 또한 handleChecked
핸들링 함수를 선언하여
위 handleInfoChange
핸들링 함수와 비슷하게 구성했다.
이벤트 타입은 같지만, 사용하지 않는다.
// file: 'EntranceInstructor.tsx'
...
<DialogActions>
<Button onClick={handleClose} color="primary">
닫기
</Button>
<Button onClick={handlePOpen} color="primary" disabled={info.token.length > 0 ? false : true}>
다음으로
</Button>
</DialogActions>
</Dialog>
DialogActions
컴포넌트는 다이얼로그 최하단의 닫기, 다음 버튼 등과 같은 이벤트를 위한 컴포넌트다.
닫기, 다음으로 버튼을 눌렀을 때 각기 핸들링 함수로 처리해줬으며,
특히 다음으로 버튼은 토큰이 생성되지 않았다면 넘어가지 않도록 비활성화했다.
입력, 토큰 생성, 체크박스 모두 잘 동작한다✨
다음 차례는 채점 기준을 설정하는 기능이다.
채점해야 할 항목이 많지만, 그만큼 중복되는 것도 많을 것이다.
이것도 어떻게 구성해야 할 지 고민해야겠다!
🔥 TroubleShooting & Review
Issue:
TextField
를 핸들링하는 것이 은근히 힘들었다. 변수를 hook으로 업데이트하고 유지하는 것에 시간을 많이 쏟음!- 토큰은 난수 생성으로 구성했는데, 겹칠 확률이 있지 않을까?라는 피드백이 있었다. 백엔드로부터 전체 토큰 리스트를 받아서 확인하기에는 오버헤드가 클 것 같은데…
- 타입 확인하는 것이 은근히 어렵다. 공부해야겠다..
Review
Dialog
방식 굉장히 맘에 든다! 찾아보니Nested Dialog
방식이 있던데, 이대로 구현하면 채점 기준 설정하는Dialog
로 바로 넘어갈 수 있지 않을까?!- 조건부 렌더링을 애용 중이다. 필요할 때만 렌더링하는 방식이 너무 좋다🥺