6. 채점 기준 설정하기
채점 기준을 만들어 저장하고 채점에 사용하자!
토큰을 생성해 클래스를 개설하는 것까지 구현했다.
그럼 엔진이 채점하기 위해 반드시 받아야 하는 기준을 어떻게 만들지 고민할 차례다.
채점 기준엔 무엇이 있을까?
가장 근본적인 문제다.
개발한 엔진에서 이미 받아야 할 채점 기준이 있기 때문에, 이를 어떻게 잘 넘겨줘야 하는지 고민했다.
// file: 'PolicyDialog.tsx'
interface PolicyProps {
state: boolean,
className: string,
instructor: string,
token: string,
itoken: string,
isDirect: boolean,
};
export default function SelectCond(props: PolicyProps) {
const initial_state = {
className: props.className,
instructor: props.instructor,
feedback: props.isDirect,
token: props.token,
itoken: props.itoken,
count: false,
compiled: false,
inputs: false,
classes: false,
packages: false,
custexc: false,
custstr: false,
interfaces: false,
superclass: false,
overriding: false,
overloading: false,
thread: false,
javadoc: false,
encapsulation: false
};
...
채점 대상이 이렇게 많다😳
먼저 PolicyProps
인터페이스는 직전의 다이얼로그에서 이어지는 정보를 받아온다.
학생용 토큰, 채점자용 토큰과 입력한 정보들 (클래스 이름과 개설자 이름), 그리고 피드백 제공 여부를 받는다.
이를 SelectCond
함수형 컴포넌트에 props
로 넘겨주고, 이제 받아야 할 채점 기준표에 같이 넣어준다.
initial_state
는 Checkbox
를 위한 변수로, 이 항목이 체크되어 채점을 고려해야 하는지 판단하도록 했다.
각 과제마다 채점 기준이 다를 수 있으므로, 각 클래스마다 원하는 채점 기준을 선택하여 표를 만들도록 했다.
// file: 'PolicyDialog.tsx'
...
const initial_data = {
className: props.className,
instructor: props.instructor,
feedback: props.isDirect,
token: props.token,
itoken: props.itoken,
point: 0,
count: { state: false } as Object,
compiled: { state: false } as Object,
runtimeCompare: { state: false } as Object,
classes : { state: false } as Object,
packages: { state: false } as Object,
customException: { state: false } as Object,
customStructure: { state: false } as Object,
inheritSuper: { state: false } as Object,
inheritInterface: { state: false } as Object,
overriding: { state: false } as Object,
overloading: { state: false } as Object,
javadoc: { state: false } as Object,
thread: { state: false } as Object,
encapsulation: { state: false } as Object,
};
const [policy, setPolicy] = useState(initial_data);
const [state, setState] = useState(initial_state);
...
이 변수는 실제 채점 기준 데이터를 저장하기 위한 변수다.
채점 고려 대상인지 확인하기 위해 state
변수를 기준에 넣어주었고, Json 객체로 만들기 위해 타입을 명시했다.
백엔드에 넘겨줄 진짜 기준표가 들어있는 policy
, state
를 각각 initial로 선언된 초기 값들로 초기화했으며, 변경점이 있으면 hook을 사용하여 업데이트해줄 것이다.
이제 컴포넌트를 만들어 볼까?
Dialog와 Checkbox
// file: 'PolicyDialog.tsx'
return (
<div>
{open &&
<Dialog
open={open}
onClose={handleClose}
aria-labelledby="form-dialog-title"
disableBackdropClick={true}
disableEscapeKeyDown={true}
maxWidth="md"
scroll='paper'
>
<DialogTitle id="form-dialog-title">
채점 기준 설정하기
</DialogTitle>
<DialogContent dividers>
<DialogContentText>
다이얼로그 내용
</DialogContentText>
<TextField
type="number"
value={policy.point}
label="총점"
size="small"
margin="dense"
onChange={e => setPolicy({ ...policy, point: (parseFloat(e.target.value) || policy.point)})}
/>
<FormGroup>
<FormControlLabel
control={
<Checkbox checked={state.count}
onChange={handleChange}
name="count" />}
label="Count"
/>
<FormControlLabel
control={
<Checkbox checked={state.compiled}
onChange={handleChange}
name="compiled" />}
label="Compile"
...
본격적으로 어려워지기 시작했다.
이전 토큰 생성 다이얼로그에서 이어지도록 이번에도 조건부 렌더링을 추가한 다이얼로그 컴포넌트로 구성했다.
채점 기준표를 작성하다가, 실수로 꺼지면 안 되므로 disableBackdropClick
, disableEscapeKeyDown
옵션을 true
로 주어
다른 데를 누르거나 ESC를 눌러 꺼지지 않도록 했다.
DialogContent
의 첫 번째 TextField
컴포넌트는 해당 과제의 점수를 입력하도록 했다.
type="number"
로 주면 숫자를 입력하는 TextField
컴포넌트가 되며, onChange
핸들링 함수를 Arrow Function으로 선언했다.
기존에 채점 기준표인 policy
를 그대로 두되, point
에 해당하는 변수만 hook을 사용하여 업데이트했다.
<FormGroup>
컴포넌트 내에서 Checkbox
컴포넌트가 채점 기준만큼 들어가는데,
위에서 선언한 Checkbox
를 위한 state
를 잘 사용해야 했다.
// file: 'PolicyDialog.tsx'
...
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.name === 'thread' || e.target.name === 'javadoc' || e.target.name === 'encapsulation') {
setPolicy(
produce(draft => {
draft[e.target.name] = e.target.checked;
})
)
}
setState(
produce(draft => {
draft[e.target.name] = e.target.checked;
})
);
}
...
채점 기준 중 스레드, 자바독, 캡슐화 항목은 감점 점수 외에 따로 추가할 값이 없으므로,
이것들을 체크하면 policy
를 직접적으로 수정하도록 했고,
나머지 항목들은 입력해야 할 값이 있으므로 일단 state
를 업데이트했다.
🔥 여기서 hook을 사용한 값 업데이트가 잘 되지 않아 고생했다. 찾아보니 값 업데이트는 절차적으로 진행되는게 아니라 비동기적으로 진행된다고 했다.
업데이트하려는 배열이 nested 구조라 건드리기도 까다로웠다.
절차적으로 코드가 수행되는데 익숙해서 많은 어려움을 겪었다. 그렇다고 불변성을 위반하면서까지 값을 강제로 넣어줄 수도 없고…
고민 끝에 찾은 해결 방법으로 immer라는 라이브러리를 사용한 것이다.
드래프트를 업데이트해 불변성을 지키면서 배열에 원하는 값을 변경해줄 수 있었다!
이렇게 각 채점 항목마다 Checkbox
컴포넌트를 선언하고, name
에 따라 불변성을 지키면서 값을 업데이트해 주도록 구성하면,
채점 기준표를 선택할 수 있게 다이얼로그가 예쁘게 생성된다.😆
상세 기준표 설정하기
Checkbox
로 구성한 이유가 드러나는 항목이다.
각 항목을 누르면 상세 기준표를 설정하는 창이 나와야 할 것이다.
이번에도 일관성을 주기 위해 다이얼로그로 구성하되, 뒤에 열려 있는 채점 기준표가 절대 이동되거나 꺼지면 안 된다!
PolicyDialog
코드가 너무 길어지는 것 같아서, 또다른 함수형 컴포넌트를 선언해 작업했다.
// file: 'app.component.policy.io.tsx'
export interface DialogRawProp {
keepMounted: boolean;
open: boolean;
onCreate: Function;
}
...
export default function InputDialog(props: DialogRawProp) {
const classes = style();
const { open: isOpen } = props;
const [open, setOpen] = useState(isOpen);
const [fields, setFields] = useState(["io-0"]);
const [outputData, setOutputData] = useState([""]);
const [inputData, setInputData] = useState([""]);
const [deduct, setDeduct] = useState(0);
const [max_deduct, setMax_deduct] = useState(0);
const [resIO, setResIO] = useState({
state: false,
input: [] as string[],
output: [] as string[],
deductPoint : 0,
maxDeduct: 0
});
...
PolicyDialog
에서 Checkbox
를 눌렀는지 확인하고, 여기서 입력한 값을 위로 올려 보내기 위해 onCreate
에서 핸들링 함수를 받는다.
이 컴포넌트는 입출력 테스트를 위한 컴포넌트로, 테스트 케이스와 그에 맞는 예상 출력 값을 입력받아야 한다.
이 테스트 케이스는 분명 여러 개일 수 있으므로, 처음부터 빈 배열로 선언해 관리한다.
// file: 'app.component.policy.io.tsx'
...
return (
<Dialog
open={open}
onClose={handleClose}
aria-labelledby="form-dialog-io"
maxWidth="md"
fullWidth={true}
scroll='paper'
disableEscapeKeyDown
>
<DialogTitle id="form-dialog-io">
테스트 케이스 작성
</DialogTitle>
<DialogContent dividers>
<DialogContentText>
다이얼로그 내용
<Button variant="outlined" onClick={() => appendFields()} startIcon={<AddIcon />} className={classes.buttonRight}>
추가
</Button>
</DialogContentText>
...
채점 기준을 만들면서 가장 어려웠던, 그러나 생각지도 못하게 쉽게 풀렸던 추가 버튼을 눌렀을 때 컴포넌트를 추가하는 것이다.
채점 기준은 하나로만 평가할 수 없기 때문에 자유롭게 추가할 수 있도록 해야 한다.
따라서 버튼을 눌렀을 때, onClick
에 맞는 핸들링 함수 appendFields()
를 실행하도록 했다.
// file: 'app.component.policy.io.tsx'
...
const [fields, setFields] = useState(["io-0"]);
...
const appendFields = () => {
let element = `io-${fields.length}`;
setFields(fields => fields.concat([element]));
}
핸들링 함수는 다음과 같다.
배열로 선언되었으며, 이 핸들링 함수가 실행되면 id
를 배열 사이즈로 새로 생성해 이어 붙여주는 것이다.
이렇게 하면 렌더링될 때 배열 fields
에서 map
을 사용해 컴포넌트를 원하는 만큼 그릴 수 있다.
// file: 'app.component.policy.io.tsx'
...
{fields.map((input, index) => (
<Grid container spacing={1} key={index}>
<Grid xs item>
<FormControl fullWidth margin="normal">
<TextField
value={inputData[index] || ""}
variant="outlined"
id={"in-" + index}
label="입력 값"
name={"in-" + index}
className="io"
multiline
onChange={handleInputChange(index)}
/>
</FormControl>
</Grid>
<Grid xs item>
<FormControl fullWidth margin="normal">
<TextField
value={outputData[index] || ""}
variant="outlined"
id={"out-" + index}
label="출력 값"
name={"out-" + index}
className="oi"
multiline
onChange={handleOutputChange(index)}
/>
</FormControl>
</Grid>
</Grid>
))}
...
map
메소드로 배열을 순회하면서 고유한 키 값으로 index
를 갖는 컴포넌트를 리턴할 수 있다.
생성한 TextField
는 각기 다른 값을 가질 수 있어야 하므로, TextField
의 value
와 onChange
핸들링 함수 또한 index
를 가지고 판별하도록 했다.
🔥 이런 방식으로 채점 항목마다 모든 컴포넌트를 만들어 주었는데, 하드 코딩이 된 것 같아 마음이 불편했다.
채점 기준이 항목마다 상이한 점이 많아서 이렇게 작성했는데, 옵션을 폭넓게 받는다면 하나로 합칠 수 있지 않을까?
데이터 올려 보내기
컴포넌트를 분리해서 개발하다 보니 데이터를 상위 컴포넌트, 즉 호출한 컴포넌트로 보내야 할 상황이 생겼다.
다른 컴포넌트를 호출할 때 데이터를 보내는 거야 쉽지만, 어떻게 올려 보낼지 고민했고, 여기에 많은 시간을 할애했다.
React를 개발하면서 데이터의 흐름이 위에서 아래로 흐르는 게 맞다는데, 이 기능에서만큼은 거슬러야 한다는 것…!
채점 기준 컴포넌트를 호출 시 onCreate
핸들링 함수를 같이 넘겨주면, 호출당한 컴포넌트에서 useEffect
hook을 사용하여
데이터를 호출한 쪽으로 넘겨줄 수 있다!
일단 채점 기준 컴포넌트를 호출할 PolicyDialog
컴포넌트를 확인하자.
// file: 'PolicyDialog.tsx'
...
const handleCreate = (types: string, data : Object) => {
setPolicy(
produce(draft => {
draft[types] = data;
})
);
}
...
호출할 때 넘겨줄 핸들링 함수다.
types
매개변수를 통해 policy
의 어떤 값을 업데이트 시켜줘야 하는지 명시하도록 했고,
드래프트를 업데이트해 불변성을 지키면서 policy
를 업데이트할 수 있다.
이 함수를 넘겨주자.
// file: 'PolicyDialog.tsx'
...
{state.compiled &&
<CompiledDialog open={state.compiled} onCreate={handleCreate} keepMounted /> }
{state.inputs &&
<InputDialog open={state.inputs} onCreate={handleCreate} keepMounted /> }
...
각 다이얼로그는 채점 기준을 설정하려고 체크박스를 눌렀을 때 뜨는 컴포넌트다.
조건부 렌더링을 사용해 체크했을 때만 다이얼로그가 렌더링되어 출력하도록 만들었다.
여기 onCreate
에 핸들링 함수를 같이 넘겨줬고, keepMounted
props를 활성화해 DOM에서 children
을 그대로 보관하고자 했다.
(keepMounted을 활성화하면 Modal
, Dialog
을 사용할 때 children을 계속 유지함으로써 응답성을 극대화할 수 있다고 한다)
이제 호출되는 컴포넌트에서 처리해주자.
// file: 'app.component.policy.io.tsx'
...
useEffect(() => {
props.onCreate("runtimeCompare", resIO);
// eslint-disable-next-line react-hooks/exhaustive-deps
},[resIO]);
...
resIO
는 여러 개의 입력한 input, output 값을 보관하는 객체다.
이 객체를 useEffect
를 사용하여 업데이트될 때마다 onCreate
로 받은 핸들링 함수를 실행시켜 값을 올려보낸다.
useEffect
는 React의 클래스형 컴포넌트에서 componentDidMount
와 비슷한 위치로, 값이 변경되면 컴포넌트를 새로 그려주거나 내부의 동작을 실행한다.
많은 채점 기준 컴포넌트를 이렇게 적용하고, 최종적으로 데이터를 받는 PolicyDialog
컴포넌트에서 백엔드로 데이터를 넘겨주면, 채점 기준이 저장된다😆
🔥 TroubleShooting & Review
Issue:
- 객체를 배열의
map
메소드로 생성하는 것부터 서로 다른 입력을 받을 수 있도록 하는 것에 많은 시간을 들였다. 특히 불변성을 신경쓰면서🤔 - 채점 기준이 14개 정도 되는데, 그에 맞는 컴포넌트를 14개 작성했다. 너무 비효율적인 것 같은데, 합칠 수 있을까?
- 데이터의 흐름을 거슬러 위로 올려보내는 것이 웹에서는 은근히 힘들었다. 애초에 자연스러운 게 아니라서 그런가…
Review
- 채점 기준을 모두
Dialog
방식으로 구현하게 되었다.Nested Dialog
로 구현하고자 했는데, 결국 뜻대로 됐다! - 백엔드와 많은 커뮤니케이션을 하면서 구현한 컴포넌트다. 데이터를 보내는 형식이 서로 맞아야 했기 때문.